Compare commits

...

No commits in common. "v0.8.0" and "main" have entirely different histories.
v0.8.0 ... main

100 changed files with 1638 additions and 5265 deletions

80
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,80 @@
name: Bug Report
description: "Report Mihomo bug"
title: "[Bug] "
labels: ["bug"]
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: Before submitting, please check all the options below to confirm that you have read and understood the following requirements; otherwise, this issue will be closed.
options:
- label: I have read the [documentation](https://wiki.metacubex.one/) and understand the meaning of all the configuration items I have written, rather than just piling up seemingly useful options or default values.
required: false
- label: I have carefully reviewed the [documentation](https://wiki.metacubex.one/) and have not resolved the issue.
required: false
- label: I have searched the [Issue Tracker](……/) for the issue I want to raise and did not find it.
required: false
- label: I am a non-Chinese user.
required: false
- label: I have tested with the latest Alpha branch version, and the issue still persists.
required: true
- label: I have provided the server and client configuration files and processes that can reproduce the issue locally, rather than a sanitized complex client configuration file.
required: true
- label: I provided the simplest configuration that can be used to reproduce the errors in my report, rather than relying on remote servers or piling on a lot of unnecessary configurations for reproduction.
required: true
- label: I have provided complete logs, rather than just the parts I think are useful out of confidence in my own intelligence.
required: true
- label: I have directly reproduced the error using the Mihomo command-line program, rather than using other tools or scripts.
required: true
- type: dropdown
attributes:
label: Operating System
description: "Please provide the type of operating system."
multiple: true
options:
- MacOS
- Windows
- Linux
- OpenBSD/FreeBSD
- Android
- type: input
attributes:
label: System Version
description: "Please provide the version of the operating system where the issue occurred."
validations:
required: true
- type: textarea
attributes:
label: Mihomo Version
description: "Provide the output of the `mihomo -v` command."
validations:
required: true
- type: textarea
attributes:
render: yaml
label: Configuration File
description: |-
Please attach the Mihomo configuration file below.
Make sure there is no sensitive information in the configuration file (such as server addresses, passwords, ports, etc.)
Also, ensure that the configuration file can reproduce the error using the Mihomo command-line program locally (if it's a proxy protocol issue, make sure the local server can be used for reproduction).
validations:
required: true
- type: textarea
attributes:
label: Description
description: "Please provide a detailed description of the error."
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: "Please provide the steps to reproduce the error."
validations:
required: true
- type: textarea
attributes:
label: Logs
description: "Attach the running logs of Mihomo Core below, with `log-level` set to `DEBUG`."
render: shell

View File

@ -0,0 +1,80 @@
name: 错误反馈
description: "提交 Mihomo 漏洞"
title: "[Bug] "
labels: ["bug"]
body:
- type: checkboxes
id: ensure
attributes:
label: 验证步骤
description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
options:
- label: 我已经阅读了 [文档](https://wiki.metacubex.one/),了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
required: false
- label: 我仔细看过 [文档](https://wiki.metacubex.one/) 并未解决问题
required: false
- label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的问题,并且没有找到
required: false
- label: 我是中文用户,而非其他语言用户
required: false
- label: 我已经使用最新的 Alpha 分支版本测试过,问题依旧存在
required: true
- label: 我提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
required: true
- label: 我提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器或者堆砌大量对于复现无用的配置等。
required: true
- label: 我提供了完整的日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
required: true
- label: 我直接使用 Mihomo 命令行程序重现了错误,而不是使用其他工具或脚本。
required: true
- type: dropdown
attributes:
label: 操作系统
description: 请提供操作系统类型
multiple: true
options:
- MacOS
- Windows
- Linux
- OpenBSD/FreeBSD
- Android
- type: input
attributes:
label: 系统版本
description: 请提供出现问题的操作系统版本
validations:
required: true
- type: textarea
attributes:
label: Mihomo 版本
description: 提供 `mihomo -v` 命令的输出
validations:
required: true
- type: textarea
attributes:
render: yaml
label: 配置文件
description: |-
在下方附上 Mihomo 配置文件
请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等)
以及确保可以在本地(如果是代理协议问题,请确保本地服务器可用于复现)使用 Mihomo 原始命令行程序重现错误的配置文件
validations:
required: true
- type: textarea
attributes:
label: 描述
description: 请提供错误的详细描述。
validations:
required: true
- type: textarea
attributes:
label: 重现方式
description: 请提供重现错误的步骤
validations:
required: true
- type: textarea
attributes:
label: 日志
description: 在下方附上 Mihomo Core 的运行日志,`log-level` 使用 `DEBUG`
render: shell

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: mihomo Community Support
url: https://github.com/MetaCubeX/mihomo/discussions
about: Please ask and answer questions about mihomo here.

View File

@ -0,0 +1,23 @@
name: Feature Request
description: Suggest improvements for this project
title: "[Feature] "
labels: ["enhancement"]
body:
- type: checkboxes
id: ensure
attributes:
label: Verification Steps
description: Before submitting, please check the following options to confirm that you have read and understood the requirements below; otherwise, this issue will be closed.
options:
- label: I have read the [documentation](https://wiki.metacubex.one/) and confirmed that this feature is not implemented
required: false
- label: I have searched for the feature request I want to propose in the [Issue Tracker](……/) and did not find it
required: false
- label: I am a non-Chinese user.
required: false
- type: textarea
attributes:
label: Description
description: Please provide a detailed description of the feature, rather than vague statements.
validations:
required: true

View File

@ -0,0 +1,23 @@
name: 功能请求
description: 为该项目提出建议
title: "[Feature] "
labels: ["enhancement"]
body:
- type: checkboxes
id: ensure
attributes:
label: 验证步骤
description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
options:
- label: 我已经阅读了 [文档](https://wiki.metacubex.one/),确认了该功能没有实现
required: false
- label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的功能请求,并且没有找到
required: false
- label: 我是中文用户,而非其他语言用户
required: false
- type: textarea
attributes:
label: 描述
description: 请提供对于该功能的详细描述,而不是莫名其妙的话术。
validations:
required: true

17
.github/workflows/Delete.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Delete old workflow
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
# Run monthly, at 00:00 on the 1st day of month.
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main
with:
token: ${{ secrets.AUTH_PAT }}
repository: ${{ github.repository }}
retain_days: 30

35
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Build
on:
workflow_dispatch:
inputs:
version:
description: "Tag version to release"
required: true
permissions:
contents: write
packages: write
jobs:
release_archive:
runs-on: ubuntu-latest
if: github.repository == 'KT-Yeh/mihomo'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: true
- name: Archive Release
uses: thedoctor0/zip-release@0.7.1
with:
type: zip
filename: 'mihomo_${{ github.ref_name }}.zip'
exclusions: '*.git*'
- name: Release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
files: 'mihomo_${{ github.ref_name }}.zip'

20
.gitignore vendored
View File

@ -1,15 +1,5 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# dep
vendor
__pycache__
venv
build
mihomo.egg-info
*.ipynb

View File

@ -1,26 +0,0 @@
language: go
sudo: false
go:
- "1.11"
install:
- "go mod download"
env:
global:
- NAME=clash
- BINDIR=bin
- GO111MODULE=on
script:
- go test ./...
before_deploy: make -j releases
deploy:
provider: releases
prerelease: true
skip_cleanup: true
api_key:
secure: dp1tc1h0er7aaAZ1hY0Xk/cUKwB0ifsAjg6e0M/Ad5NC87oucP6ESNFkDu0e9rUS1yB826/VnVGzNE/Z5zdjXVzPft+g5v5oRxzI4BKLhf07t9s+x8Z+3sApTxdsC5BvcN9x+5yRbpDLQ3biDPxSFu86j7m2pkEWw6XYNZO3/5y+RZXX7zu+d4MzTLUaA2kWl7KQAP0tEJNuw9ACDhpkw7LYbU/8q3E76prOTeme5/AT6Gxj7XhKUNP27lazhhqBSWM14ybPANqojNLEfMFHN/Eu2phYO07MuLTd4zuOIuw9y65kgvTFcHRlORjwUhnviXyA69obQejjgDI1WDOtU4PqpFaSLrxWtKI6k5VNWHARYggDm/wKl0WG7F0Kgio1KiGGhDg2yrbseXr/zBNaDhBtTFh6XJffqqwmgby1PXB6PWwfvWXooJMaQiFZczLWeMBl8v6XbSN6jtMTh/PQlKai6BcDd4LM8GQ7VHpSeff4qXEU4Vpnadjgs8VDPOHng6/HV+wDs8q2LrlMbnxLWxbCjOMUB6w7YnSrwH9owzKSoUs/531I4tTCRQIgipJtTK2b881/8osVjdMGS1mDXhBWO+OM0LCAdORJz+kN4PIkXXvKLt6jX74k6z4M3swFaqqtlTduN2Yy/ErsjguQO1VZfHmcpNssmJXI5QB9sxA=
file: bin/*
file_glob: true
on:
repo: Dreamacro/clash
branch: master
tags: true

View File

@ -1,17 +0,0 @@
FROM golang:latest as builder
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
WORKDIR /clash-src
COPY . /clash-src
RUN go mod download && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash && \
chmod +x /clash
FROM alpine:latest
RUN apk --no-cache add ca-certificates && \
mkdir -p /root/.config/clash
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash .
EXPOSE 7890 7891
ENTRYPOINT ["/clash"]

22
LICENSE
View File

@ -1,21 +1,7 @@
MIT License
Copyright 2023 KT
Copyright (c) 2018 Dreamacro
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,23 +0,0 @@
NAME=clash
BINDIR=bin
GOBUILD=CGO_ENABLED=0 go build -ldflags '-w -s'
all: linux macos win64
linux:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
macos:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
win64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
releases: linux macos win64
chmod +x $(BINDIR)/$(NAME)-*
gzip $(BINDIR)/$(NAME)-linux
gzip $(BINDIR)/$(NAME)-macos
zip -m -j $(BINDIR)/$(NAME)-win64.zip $(BINDIR)/$(NAME)-win64.exe
clean:
rm $(BINDIR)/*

231
README.md
View File

@ -1,158 +1,115 @@
<h1 align="center">
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
<br>
Clash
<br>
</h1>
# mihomo
A simple python pydantic model (type hint and autocompletion support) for Honkai: Star Rail parsed data from the Mihomo API.
<h4 align="center">A rule based proxy in Go.</h4>
API url: https://api.mihomo.me/sr_info_parsed/{UID}?lang={LANG}
<p align="center">
<a href="https://travis-ci.org/Dreamacro/clash">
<img src="https://img.shields.io/travis/Dreamacro/clash.svg?style=flat-square"
alt="Travis-CI">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a>
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
</p>
## Features
- HTTP/HTTPS and SOCKS proxy
- Surge like configuration
- GeoIP rule support
- Support Vmess/Shadowsocks/Socks5
- Support for Netfilter TCP redirect
## Discussion
[Telegram Group](https://t.me/clash_discuss)
## Install
You can build from source:
```sh
go get -u -v github.com/Dreamacro/clash
## Installation
```
pip install -U git+https://github.com/KT-Yeh/mihomo.git
```
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
## Usage
Requires Go >= 1.10.
### Basic
There are two parsed data formats:
- V1:
- URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en&version=v1
- Fetching: use `client.fetch_user_v1(800333171)`
- Data model: `mihomo.models.v1.StarrailInfoParsedV1`
- All models defined in `mihomo/models/v1` directory.
- V2:
- URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en
- Fetching: use `client.fetch_user(800333171)`
- Data model: `mihomo.models.StarrailInfoParsed`
- All models defined in `mihomo/models` directory.
## Daemon
If you don't want to use `client.get_icon_url` to get the image url everytime, you can use `client.fetch_user(800333171, replace_icon_name_with_url=True)` to get the parsed data with asset urls.
Unfortunately, there is no native elegant way to implement golang's daemon.
### Example
```py
import asyncio
So we can use third-party daemon tools like pm2, supervisor, and so on.
from mihomo import Language, MihomoAPI
from mihomo.models import StarrailInfoParsed
from mihomo.models.v1 import StarrailInfoParsedV1
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
client = MihomoAPI(language=Language.EN)
```sh
pm2 start clash
async def v1():
data: StarrailInfoParsedV1 = await client.fetch_user_v1(800333171)
print(f"Name: {data.player.name}")
print(f"Level: {data.player.level}")
print(f"Signature: {data.player.signature}")
print(f"Achievements: {data.player_details.achievements}")
print(f"Characters count: {data.player_details.characters}")
print(f"Profile picture url: {client.get_icon_url(data.player.icon)}")
for character in data.characters:
print("-----------")
print(f"Name: {character.name}")
print(f"Rarity: {character.rarity}")
print(f"Level: {character.level}")
print(f"Avatar url: {client.get_icon_url(character.icon)}")
print(f"Preview url: {client.get_icon_url(character.preview)}")
print(f"Portrait url: {client.get_icon_url(character.portrait)}")
async def v2():
data: StarrailInfoParsed = await client.fetch_user(800333171, replace_icon_name_with_url=True)
print(f"Name: {data.player.name}")
print(f"Level: {data.player.level}")
print(f"Signature: {data.player.signature}")
print(f"Profile picture url: {data.player.avatar.icon}")
for character in data.characters:
print("-----------")
print(f"Name: {character.name}")
print(f"Rarity: {character.rarity}")
print(f"Portrait url: {character.portrait}")
asyncio.run(v1())
asyncio.run(v2())
```
If you have Docker installed, you can run clash directly using `docker-compose`.
[Run clash in docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
## Config
**NOTE: after v0.8.0, clash using yaml as configuration file**
The default configuration directory is `$HOME/.config/clash`
The name of the configuration file is `config.yml`
If you want to use another directory, you can use `-d` to control the configuration directory
For example, you can use the current directory as the configuration directory
```sh
clash -d .
### Tools
`from mihomo import tools`
#### Remove Duplicate Character
```py
data = await client.fetch_user(800333171)
data = tools.remove_duplicate_character(data)
```
Below is a simple demo configuration file:
#### Merge Character Data
```py
old_data = await client.fetch_user(800333171)
```yml
# port of HTTP
port: 7890
# Change characters in game and wait for the API to refresh
# ...
# port of SOCKS5
socks-port: 7891
# redir proxy for Linux and macOS
# redir-port: 7892
allow-lan: false
# Rule / Global/ Direct (default is Rule)
mode: Rule
# set log level to stdout (default is info)
# info / warning / error / debug
log-level: info
# A RESTful API for clash
external-controller: 127.0.0.1:9090
# Secret for RESTful API (Optional)
secret: ""
Proxy:
# shadowsocks
# The types of cipher are consistent with go-shadowsocks2
# support AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CTR AES-192-CTR AES-256-CTR AES-128-CFB AES-192-CFB AES-256-CFB CHACHA20-IETF XCHACHA20
# In addition to what go-shadowsocks2 supports, it also supports chacha20 rc4-md5 xchacha20-ietf-poly1305
- { name: "ss1", type: ss, server: server, port: 443, cipher: AEAD_CHACHA20_POLY1305, password: "password" }
- { name: "ss2", type: ss, server: server, port: 443, cipher: AEAD_CHACHA20_POLY1305, password: "password", obfs: tls, obfs-host: bing.com }
# vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
- { name: "vmess1", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto }
- { name: "vmess2", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true }
# socks5
- { name: "socks", type: socks5, server: server, port: 443 }
Proxy Group:
# url-test select which proxy will be used by benchmarking speed to a URL.
- { name: "auto", type: url-test, proxies: ["ss1", "ss2", "vmess1"], url: http://www.gstatic.com/generate_204, interval: 300 }
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
- { name: "fallback-auto", type: fallback, proxies: ["ss1", "ss2", "vmess1"], url: http://www.gstatic.com/generate_204, interval: 300 }
# select is used for selecting proxy or proxy group
# you can use RESTful API to switch proxy, is recommended for use in GUI.
- { name: "Proxy", type: select, proxies: ["ss1", "ss2", "vmess1", "auto"] }
Rule:
- DOMAIN-SUFFIX,google.com,Proxy
- DOMAIN-KEYWORD,google,Proxy
- DOMAIN-SUFFIX,ad.com,REJECT
- GEOIP,CN,DIRECT
# note: there is two ","
- FINAL,,Proxy
new_data = await client.fetch_user(800333171)
data = tools.merge_character_data(new_data, old_data)
```
## Thanks
### Data Persistence
Take pickle and json as an example
```py
import pickle
import zlib
from mihomo import MihomoAPI, Language, StarrailInfoParsed
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
client = MihomoAPI(language=Language.EN)
data = await client.fetch_user(800333171)
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
# Save
pickle_data = zlib.compress(pickle.dumps(data))
print(len(pickle_data))
json_data = data.json(by_alias=True, ensure_ascii=False)
print(len(json_data))
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [ ] UDP support
- [ ] Connection manager
# Load
data_from_pickle = pickle.loads(zlib.decompress(pickle_data))
data_from_json = StarrailInfoParsed.parse_raw(json_data)
print(type(data_from_pickle))
print(type(data_from_json))
```

View File

@ -1,64 +0,0 @@
package adapters
import (
"net"
"net/http"
"strings"
C "github.com/Dreamacro/clash/constant"
)
// HTTPAdapter is a adapter for HTTP connection
type HTTPAdapter struct {
metadata *C.Metadata
conn net.Conn
R *http.Request
}
// Close HTTP connection
func (h *HTTPAdapter) Close() {
h.conn.Close()
}
// Metadata return destination metadata
func (h *HTTPAdapter) Metadata() *C.Metadata {
return h.metadata
}
// Conn return raw net.Conn of HTTP
func (h *HTTPAdapter) Conn() net.Conn {
return h.conn
}
// NewHTTP is HTTPAdapter generator
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
return &HTTPAdapter{
metadata: parseHTTPAddr(request),
R: request,
conn: conn,
}
}
// RemoveHopByHopHeaders remove hop-by-hop header
func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
header.Del("Proxy-Connection")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
connections := header.Get("Connection")
header.Del("Connection")
if len(connections) == 0 {
return
}
for _, h := range strings.Split(connections, ",") {
header.Del(strings.TrimSpace(h))
}
}

View File

@ -1,14 +0,0 @@
package adapters
import (
"net"
"net/http"
)
// NewHTTPS is HTTPAdapter generator
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
return &SocketAdapter{
metadata: parseHTTPAddr(request),
conn: conn,
}
}

View File

@ -1,37 +0,0 @@
package adapters
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// SocketAdapter is a adapter for socks and redir connection
type SocketAdapter struct {
conn net.Conn
metadata *C.Metadata
}
// Close socks and redir connection
func (s *SocketAdapter) Close() {
s.conn.Close()
}
// Metadata return destination metadata
func (s *SocketAdapter) Metadata() *C.Metadata {
return s.metadata
}
// Conn return raw net.Conn
func (s *SocketAdapter) Conn() net.Conn {
return s.conn
}
// NewSocket is SocketAdapter generator
func NewSocket(target socks.Addr, conn net.Conn) *SocketAdapter {
return &SocketAdapter{
conn: conn,
metadata: parseSocksAddr(target),
}
}

View File

@ -1,71 +0,0 @@
package adapters
import (
"net"
"net/http"
"strconv"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parseSocksAddr(target socks.Addr) *C.Metadata {
var host, port string
var ip net.IP
switch target[0] {
case socks.AtypDomainName:
host = string(target[2 : 2+target[1]])
port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
ipAddr, err := net.ResolveIPAddr("ip", host)
if err == nil {
ip = ipAddr.IP
}
case socks.AtypIPv4:
ip = net.IP(target[1 : 1+net.IPv4len])
port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks.AtypIPv6:
ip = net.IP(target[1 : 1+net.IPv6len])
port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return &C.Metadata{
NetWork: C.TCP,
AddrType: int(target[0]),
Host: host,
IP: &ip,
Port: port,
}
}
func parseHTTPAddr(request *http.Request) *C.Metadata {
host := request.URL.Hostname()
port := request.URL.Port()
if port == "" {
port = "80"
}
ipAddr, err := net.ResolveIPAddr("ip", host)
var resolveIP *net.IP
if err == nil {
resolveIP = &ipAddr.IP
}
var addType int
ip := net.ParseIP(host)
switch {
case ip == nil:
addType = socks.AtypDomainName
case ip.To4() == nil:
addType = socks.AtypIPv6
default:
addType = socks.AtypIPv4
}
return &C.Metadata{
NetWork: C.TCP,
AddrType: addType,
Host: host,
IP: resolveIP,
Port: port,
}
}

View File

@ -1,45 +0,0 @@
package adapters
import (
"net"
C "github.com/Dreamacro/clash/constant"
)
// DirectAdapter is a directly connected adapter
type DirectAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (d *DirectAdapter) Close() {
d.conn.Close()
}
// Conn is used to http request
func (d *DirectAdapter) Conn() net.Conn {
return d.conn
}
type Direct struct{}
func (d *Direct) Name() string {
return "Direct"
}
func (d *Direct) Type() C.AdapterType {
return C.Direct
}
func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", net.JoinHostPort(metadata.String(), metadata.Port))
if err != nil {
return
}
tcpKeepAlive(c)
return &DirectAdapter{conn: c}, nil
}
func NewDirect() *Direct {
return &Direct{}
}

View File

@ -1,136 +0,0 @@
package adapters
import (
"errors"
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
)
type proxy struct {
RawProxy C.Proxy
Valid bool
}
type Fallback struct {
name string
proxies []*proxy
rawURL string
interval time.Duration
done chan struct{}
}
type FallbackOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
URL string `proxy:"url"`
Interval int `proxy:"interval"`
}
func (f *Fallback) Name() string {
return f.name
}
func (f *Fallback) Type() C.AdapterType {
return C.Fallback
}
func (f *Fallback) Now() string {
_, proxy := f.findNextValidProxy(0)
if proxy != nil {
return proxy.RawProxy.Name()
}
return f.proxies[0].RawProxy.Name()
}
func (f *Fallback) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
idx := 0
var proxy *proxy
for {
idx, proxy = f.findNextValidProxy(idx)
if proxy == nil {
break
}
adapter, err = proxy.RawProxy.Generator(metadata)
if err != nil {
proxy.Valid = false
idx++
continue
}
return
}
return f.proxies[0].RawProxy.Generator(metadata)
}
func (f *Fallback) Close() {
f.done <- struct{}{}
}
func (f *Fallback) loop() {
tick := time.NewTicker(f.interval)
go f.validTest()
Loop:
for {
select {
case <-tick.C:
go f.validTest()
case <-f.done:
break Loop
}
}
}
func (f *Fallback) findNextValidProxy(start int) (int, *proxy) {
for i := start; i < len(f.proxies); i++ {
if f.proxies[i].Valid {
return i, f.proxies[i]
}
}
return -1, nil
}
func (f *Fallback) validTest() {
wg := sync.WaitGroup{}
wg.Add(len(f.proxies))
for _, p := range f.proxies {
go func(p *proxy) {
_, err := DelayTest(p.RawProxy, f.rawURL)
p.Valid = err == nil
wg.Done()
}(p)
}
wg.Wait()
}
func NewFallback(option FallbackOption, proxies []C.Proxy) (*Fallback, error) {
_, err := urlToMetadata(option.URL)
if err != nil {
return nil, err
}
if len(proxies) < 1 {
return nil, errors.New("The number of proxies cannot be 0")
}
interval := time.Duration(option.Interval) * time.Second
warpperProxies := make([]*proxy, len(proxies))
for idx := range proxies {
warpperProxies[idx] = &proxy{
RawProxy: proxies[idx],
Valid: true,
}
}
Fallback := &Fallback{
name: option.Name,
proxies: warpperProxies,
rawURL: option.URL,
interval: interval,
done: make(chan struct{}),
}
go Fallback.loop()
return Fallback, nil
}

View File

@ -1,69 +0,0 @@
package adapters
import (
"io"
"net"
"time"
C "github.com/Dreamacro/clash/constant"
)
// RejectAdapter is a reject connected adapter
type RejectAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (r *RejectAdapter) Close() {}
// Conn is used to http request
func (r *RejectAdapter) Conn() net.Conn {
return r.conn
}
type Reject struct {
}
func (r *Reject) Name() string {
return "Reject"
}
func (r *Reject) Type() C.AdapterType {
return C.Reject
}
func (r *Reject) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
return &RejectAdapter{conn: &NopConn{}}, nil
}
func NewReject() *Reject {
return &Reject{}
}
type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) {
return len(b), nil
}
func (rw *NopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
// Close is fake function for net.Conn
func (rw *NopConn) Close() error { return nil }
// LocalAddr is fake function for net.Conn
func (rw *NopConn) LocalAddr() net.Addr { return nil }
// RemoteAddr is fake function for net.Conn
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
// SetDeadline is fake function for net.Conn
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
// SetReadDeadline is fake function for net.Conn
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
// SetWriteDeadline is fake function for net.Conn
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -1,72 +0,0 @@
package adapters
import (
"errors"
"sort"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
name string
selected C.Proxy
proxies map[string]C.Proxy
}
type SelectorOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
}
func (s *Selector) Name() string {
return s.name
}
func (s *Selector) Type() C.AdapterType {
return C.Selector
}
func (s *Selector) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
return s.selected.Generator(metadata)
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) All() []string {
var all []string
for k := range s.proxies {
all = append(all, k)
}
sort.Strings(all)
return all
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxies[name]
if !exist {
return errors.New("Proxy does not exist")
}
s.selected = proxy
return nil
}
func NewSelector(name string, proxies map[string]C.Proxy) (*Selector, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
}
mapping := make(map[string]C.Proxy)
var init string
for k, v := range proxies {
mapping[k] = v
init = k
}
s := &Selector{
name: name,
proxies: mapping,
selected: proxies[init],
}
return s, nil
}

View File

@ -1,116 +0,0 @@
package adapters
import (
"bytes"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/simple-obfs"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// ShadowsocksAdapter is a shadowsocks adapter
type ShadowsocksAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *ShadowsocksAdapter) Close() {
ss.conn.Close()
}
func (ss *ShadowsocksAdapter) Conn() net.Conn {
return ss.conn
}
type ShadowSocks struct {
server string
name string
obfs string
obfsHost string
cipher core.Cipher
}
type ShadowSocksOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
Obfs string `proxy:"obfs,omitempty"`
ObfsHost string `proxy:"obfs-host,omitempty"`
}
func (ss *ShadowSocks) Name() string {
return ss.name
}
func (ss *ShadowSocks) Type() C.AdapterType {
return C.Shadowsocks
}
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
}
tcpKeepAlive(c)
switch ss.obfs {
case "tls":
c = obfs.NewTLSObfs(c, ss.obfsHost)
case "http":
_, port, _ := net.SplitHostPort(ss.server)
c = obfs.NewHTTPObfs(c, ss.obfsHost, port)
}
c = ss.cipher.StreamConn(c)
_, err = c.Write(serializesSocksAddr(metadata))
return &ShadowsocksAdapter{conn: c}, err
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
server := fmt.Sprintf("%s:%d", option.Server, option.Port)
cipher := option.Cipher
password := option.Password
ciph, err := core.PickCipher(cipher, nil, password)
if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
}
obfs := option.Obfs
obfsHost := "bing.com"
if option.ObfsHost != "" {
obfsHost = option.ObfsHost
}
return &ShadowSocks{
server: server,
name: option.Name,
cipher: ciph,
obfs: obfs,
obfsHost: obfsHost,
}, nil
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte
aType := uint8(metadata.AddrType)
p, _ := strconv.Atoi(metadata.Port)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks.AtypDomainName:
len := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
case socks.AtypIPv4:
host := metadata.IP.To4()
buf = [][]byte{{aType}, host, port}
case socks.AtypIPv6:
host := metadata.IP.To16()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
}

View File

@ -1,96 +0,0 @@
package adapters
import (
"bytes"
"errors"
"fmt"
"io"
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// Socks5Adapter is a shadowsocks adapter
type Socks5Adapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *Socks5Adapter) Close() {
ss.conn.Close()
}
func (ss *Socks5Adapter) Conn() net.Conn {
return ss.conn
}
type Socks5 struct {
addr string
name string
}
type Socks5Option struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
}
func (ss *Socks5) Name() string {
return ss.name
}
func (ss *Socks5) Type() C.AdapterType {
return C.Socks5
}
func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.addr)
}
tcpKeepAlive(c)
if err := ss.shakeHand(metadata, c); err != nil {
return nil, err
}
return &Socks5Adapter{conn: c}, nil
}
func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
buf := make([]byte, socks.MaxAddrLen)
// VER, CMD, RSV
_, err := rw.Write([]byte{5, 1, 0})
if err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err
}
if buf[0] != 5 {
return errors.New("SOCKS version error")
} else if buf[1] != 0 {
return errors.New("SOCKS need auth")
}
// VER, CMD, RSV, ADDR
if _, err := rw.Write(bytes.Join([][]byte{{5, 1, 0}, serializesSocksAddr(metadata)}, []byte(""))); err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
return err
}
return nil
}
func NewSocks5(option Socks5Option) *Socks5 {
return &Socks5{
addr: fmt.Sprintf("%s:%d", option.Server, option.Port),
name: option.Name,
}
}

View File

@ -1,114 +0,0 @@
package adapters
import (
"errors"
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
name string
proxies []C.Proxy
rawURL string
fast C.Proxy
interval time.Duration
done chan struct{}
}
type URLTestOption struct {
Name string `proxy:"name"`
Proxies []string `proxy:"proxies"`
URL string `proxy:"url"`
Interval int `proxy:"interval"`
}
func (u *URLTest) Name() string {
return u.name
}
func (u *URLTest) Type() C.AdapterType {
return C.URLTest
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
return u.fast.Generator(metadata)
}
func (u *URLTest) Close() {
u.done <- struct{}{}
}
func (u *URLTest) loop() {
tick := time.NewTicker(u.interval)
go u.speedTest()
Loop:
for {
select {
case <-tick.C:
go u.speedTest()
case <-u.done:
break Loop
}
}
}
func (u *URLTest) speedTest() {
wg := sync.WaitGroup{}
wg.Add(len(u.proxies))
c := make(chan interface{})
fast := selectFast(c)
timer := time.NewTimer(u.interval)
for _, p := range u.proxies {
go func(p C.Proxy) {
_, err := DelayTest(p, u.rawURL)
if err == nil {
c <- p
}
wg.Done()
}(p)
}
go func() {
wg.Wait()
close(c)
}()
select {
case <-timer.C:
// Wait for fast to return or close.
<-fast
case p, open := <-fast:
if open {
u.fast = p.(C.Proxy)
}
}
}
func NewURLTest(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
_, err := urlToMetadata(option.URL)
if err != nil {
return nil, err
}
if len(proxies) < 1 {
return nil, errors.New("The number of proxies cannot be 0")
}
interval := time.Duration(option.Interval) * time.Second
urlTest := &URLTest{
name: option.Name,
proxies: proxies[:],
rawURL: option.URL,
fast: proxies[0],
interval: interval,
done: make(chan struct{}),
}
go urlTest.loop()
return urlTest, nil
}

View File

@ -1,93 +0,0 @@
package adapters
import (
"fmt"
"net"
"net/http"
"net/url"
"time"
C "github.com/Dreamacro/clash/constant"
)
// DelayTest get the delay for the specified URL
func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
addr, err := urlToMetadata(url)
if err != nil {
return
}
start := time.Now()
instance, err := proxy.Generator(&addr)
if err != nil {
return
}
defer instance.Close()
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance.Conn(), nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
resp, err := client.Get(url)
if err != nil {
return
}
resp.Body.Close()
t = int16(time.Since(start) / time.Millisecond)
return
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
if u.Scheme == "https" {
port = "443"
} else if u.Scheme == "http" {
port = "80"
} else {
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
IP: nil,
Port: port,
}
return
}
func selectFast(in chan interface{}) chan interface{} {
out := make(chan interface{})
go func() {
p, open := <-in
if open {
out <- p
}
close(out)
for range in {
}
}()
return out
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
}
}

View File

@ -1,105 +0,0 @@
package adapters
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/vmess"
C "github.com/Dreamacro/clash/constant"
)
// VmessAdapter is a vmess adapter
type VmessAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (v *VmessAdapter) Close() {
v.conn.Close()
}
func (v *VmessAdapter) Conn() net.Conn {
return v.conn
}
type Vmess struct {
name string
server string
client *vmess.Client
}
type VmessOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
TLS bool `proxy:"tls,omitempty"`
}
func (ss *Vmess) Name() string {
return ss.name
}
func (ss *Vmess) Type() C.AdapterType {
return C.Vmess
}
func (ss *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
}
tcpKeepAlive(c)
c = ss.client.New(c, parseVmessAddr(metadata))
return &VmessAdapter{conn: c}, err
}
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
TLS: option.TLS,
})
if err != nil {
return nil, err
}
return &Vmess{
name: option.Name,
server: fmt.Sprintf("%s:%d", option.Server, option.Port),
client: client,
}, nil
}
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
var addrType byte
var addr []byte
switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.IP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.IP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.Atoi(metadata.Port)
return &vmess.DstAddr{
AddrType: addrType,
Addr: addr,
Port: uint(port),
}
}

View File

@ -1,3 +0,0 @@
package observable
type Iterable <-chan interface{}

View File

@ -1,67 +0,0 @@
package observable
import (
"errors"
"sync"
)
type Observable struct {
iterable Iterable
listener *sync.Map
done bool
doneLock sync.RWMutex
}
func (o *Observable) process() {
for item := range o.iterable {
o.listener.Range(func(key, value interface{}) bool {
elm := value.(*Subscriber)
elm.Emit(item)
return true
})
}
o.close()
}
func (o *Observable) close() {
o.doneLock.Lock()
o.done = true
o.doneLock.Unlock()
o.listener.Range(func(key, value interface{}) bool {
elm := value.(*Subscriber)
elm.Close()
return true
})
}
func (o *Observable) Subscribe() (Subscription, error) {
o.doneLock.RLock()
done := o.done
o.doneLock.RUnlock()
if done == true {
return nil, errors.New("Observable is closed")
}
subscriber := newSubscriber()
o.listener.Store(subscriber.Out(), subscriber)
return subscriber.Out(), nil
}
func (o *Observable) UnSubscribe(sub Subscription) {
elm, exist := o.listener.Load(sub)
if !exist {
return
}
subscriber := elm.(*Subscriber)
o.listener.Delete(subscriber.Out())
subscriber.Close()
}
func NewObservable(any Iterable) *Observable {
observable := &Observable{
iterable: any,
listener: &sync.Map{},
}
go observable.process()
return observable
}

View File

@ -1,124 +0,0 @@
package observable
import (
"runtime"
"sync"
"testing"
"time"
)
func iterator(item []interface{}) chan interface{} {
ch := make(chan interface{})
go func() {
time.Sleep(100 * time.Millisecond)
for _, elm := range item {
ch <- elm
}
close(ch)
}()
return ch
}
func TestObservable(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
if err != nil {
t.Error(err)
}
count := 0
for range data {
count = count + 1
}
if count != 5 {
t.Error("Revc number error")
}
}
func TestObservable_MutilSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe()
count := 0
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan interface{}) {
for range ch {
count = count + 1
}
wg.Done()
}
go waitCh(ch1)
go waitCh(ch2)
wg.Wait()
if count != 10 {
t.Error("Revc number error")
}
}
func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
if err != nil {
t.Error(err)
}
src.UnSubscribe(data)
_, open := <-data
if open {
t.Error("Revc number error")
}
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1})
src := NewObservable(iter)
data, _ := src.Subscribe()
<-data
_, closed := src.Subscribe()
if closed == nil {
t.Error("Observable should be closed")
}
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{}))
iter := iterator([]interface{}{1})
src := NewObservable(iter)
src.UnSubscribe(sub)
}
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
// waiting for other goroutine recycle
time.Sleep(120 * time.Millisecond)
init := runtime.NumGoroutine()
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
max := 100
var list []Subscription
for i := 0; i < max; i++ {
ch, _ := src.Subscribe()
list = append(list, ch)
}
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan interface{}) {
for range ch {
}
wg.Done()
}
for _, ch := range list {
go waitCh(ch)
}
wg.Wait()
now := runtime.NumGoroutine()
if init != now {
t.Errorf("Goroutine Leak: init %d now %d", init, now)
}
}

View File

@ -1,35 +0,0 @@
package observable
import (
"sync"
"gopkg.in/eapache/channels.v1"
)
type Subscription <-chan interface{}
type Subscriber struct {
buffer *channels.InfiniteChannel
once sync.Once
}
func (s *Subscriber) Emit(item interface{}) {
s.buffer.In() <- item
}
func (s *Subscriber) Out() Subscription {
return s.buffer.Out()
}
func (s *Subscriber) Close() {
s.once.Do(func() {
s.buffer.Close()
})
}
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: channels.NewInfiniteChannel(),
}
return sub
}

View File

@ -1,160 +0,0 @@
package structure
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// Option is the configuration that is used to create a new decoder
type Option struct {
TagName string
WeaklyTypedInput bool
}
// Decoder is the core of structure
type Decoder struct {
option *Option
}
// NewDecoder return a Decoder by Option
func NewDecoder(option Option) *Decoder {
if option.TagName == "" {
option.TagName = "structure"
}
return &Decoder{option: &option}
}
// Decode transform a map[string]interface{} to a struct
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("Decode must recive a ptr struct")
}
t := reflect.TypeOf(dst).Elem()
v := reflect.ValueOf(dst).Elem()
for idx := 0; idx < v.NumField(); idx++ {
field := t.Field(idx)
tag := field.Tag.Get(d.option.TagName)
str := strings.SplitN(tag, ",", 2)
key := str[0]
omitempty := false
if len(str) > 1 {
omitempty = str[1] == "omitempty"
}
value, ok := src[key]
if !ok {
if omitempty {
continue
}
return fmt.Errorf("key %s missing", key)
}
err := d.decode(key, value, v.Field(idx))
if err != nil {
return err
}
}
return nil
}
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error {
switch val.Kind() {
case reflect.Int:
return d.decodeInt(name, data, val)
case reflect.String:
return d.decodeString(name, data, val)
case reflect.Bool:
return d.decodeBool(name, data, val)
case reflect.Slice:
return d.decodeSlice(name, data, val)
default:
return fmt.Errorf("type %s not support", val.Kind().String())
}
}
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
case kind == reflect.Int:
val.SetInt(dataVal.Int())
case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
if err == nil {
val.SetInt(i)
} else {
err = fmt.Errorf("cannot parse '%s' as int: %s", name, err)
}
default:
err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
return err
}
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
case kind == reflect.String:
val.SetString(dataVal.String())
case kind == reflect.Int && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatInt(dataVal.Int(), 10))
default:
err = fmt.Errorf(
"'%s' expected type'%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
return err
}
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
case kind == reflect.Bool:
val.SetBool(dataVal.Bool())
case kind == reflect.Int && d.option.WeaklyTypedInput:
val.SetBool(dataVal.Int() != 0)
default:
err = fmt.Errorf(
"'%s' expected type'%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
return err
}
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
valType := val.Type()
valElemType := valType.Elem()
if dataVal.Kind() != reflect.Slice {
return fmt.Errorf("'%s' is not a slice", name)
}
valSlice := val
for i := 0; i < dataVal.Len(); i++ {
currentData := dataVal.Index(i).Interface()
for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
}
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i)
if err := d.decode(fieldName, currentData, currentField); err != nil {
return err
}
}
val.Set(valSlice)
return nil
}

View File

@ -1,141 +0,0 @@
package structure
import (
"reflect"
"testing"
)
var decoder = NewDecoder(Option{TagName: "test"})
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
type Baz struct {
Foo int `test:"foo"`
Bar string `test:"bar"`
}
type BazSlice struct {
Foo int `test:"foo"`
Bar []string `test:"bar"`
}
type BazOptional struct {
Foo int `test:"foo,omitempty"`
Bar string `test:"bar,omitempty"`
}
func TestStructure_Basic(t *testing.T) {
rawMap := map[string]interface{}{
"foo": 1,
"bar": "test",
"extra": false,
}
goal := &Baz{
Foo: 1,
Bar: "test",
}
s := &Baz{}
err := decoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}
func TestStructure_Slice(t *testing.T) {
rawMap := map[string]interface{}{
"foo": 1,
"bar": []string{"one", "two"},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"one", "two"},
}
s := &BazSlice{}
err := decoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}
func TestStructure_Optional(t *testing.T) {
rawMap := map[string]interface{}{
"foo": 1,
}
goal := &BazOptional{
Foo: 1,
}
s := &BazOptional{}
err := decoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}
func TestStructure_MissingKey(t *testing.T) {
rawMap := map[string]interface{}{
"foo": 1,
}
s := &Baz{}
err := decoder.Decode(rawMap, s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
}
func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]interface{}{}
s := Baz{}
err := decoder.Decode(rawMap, s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
}
func TestStructure_SliceTypeError(t *testing.T) {
rawMap := map[string]interface{}{
"foo": 1,
"bar": []int{1, 2},
}
s := &BazSlice{}
err := decoder.Decode(rawMap, s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
}
func TestStructure_WeakType(t *testing.T) {
rawMap := map[string]interface{}{
"foo": "1",
"bar": []int{1},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"1"},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}

View File

@ -1,88 +0,0 @@
package obfs
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
)
// HTTPObfs is shadowsocks http simple-obfs implementation
type HTTPObfs struct {
net.Conn
host string
port string
buf []byte
offset int
firstRequest bool
firstResponse bool
}
func (ho *HTTPObfs) Read(b []byte) (int, error) {
if ho.buf != nil {
n := copy(b, ho.buf[ho.offset:])
ho.offset += n
if ho.offset == len(ho.buf) {
ho.buf = nil
}
return n, nil
}
if ho.firstResponse {
buf := bufPool.Get().([]byte)
n, err := ho.Conn.Read(buf)
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if idx == -1 {
bufPool.Put(buf[:cap(buf)])
return 0, io.EOF
}
ho.firstResponse = false
length := n - (idx + 4)
n = copy(b, buf[idx+4:n])
if length > n {
ho.buf = buf[:idx+4+length]
ho.offset = idx + 4 + n
} else {
bufPool.Put(buf[:cap(buf)])
}
return n, nil
}
return ho.Conn.Read(b)
}
func (ho *HTTPObfs) Write(b []byte) (int, error) {
if ho.firstRequest {
randBytes := make([]byte, 16)
rand.Read(randBytes)
req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:]))
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2))
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port)
req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
req.ContentLength = int64(len(b))
err := req.Write(ho.Conn)
ho.firstRequest = false
return len(b), err
}
return ho.Conn.Write(b)
}
// NewHTTPObfs return a HTTPObfs
func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn {
return &HTTPObfs{
Conn: conn,
firstRequest: true,
firstResponse: true,
host: host,
port: port,
}
}

View File

@ -1,190 +0,0 @@
package obfs
import (
"bytes"
"encoding/binary"
"io"
"math/rand"
"net"
"sync"
"time"
)
func init() {
rand.Seed(time.Now().Unix())
}
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 2048) }}
// TLSObfs is shadowsocks tls simple-obfs implementation
type TLSObfs struct {
net.Conn
server string
remain int
firstRequest bool
firstResponse bool
}
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
buf := bufPool.Get().([]byte)
_, err := io.ReadFull(to.Conn, buf[:discardN])
if err != nil {
return 0, err
}
bufPool.Put(buf[:cap(buf)])
sizeBuf := make([]byte, 2)
_, err = io.ReadFull(to.Conn, sizeBuf)
if err != nil {
return 0, nil
}
length := int(binary.BigEndian.Uint16(sizeBuf))
if length > len(b) {
n, err := to.Conn.Read(b)
if err != nil {
return n, err
}
to.remain = length - n
return n, nil
}
return io.ReadFull(to.Conn, b[:length])
}
func (to *TLSObfs) Read(b []byte) (int, error) {
if to.remain > 0 {
length := to.remain
if length > len(b) {
length = len(b)
}
n, err := io.ReadFull(to.Conn, b[:length])
to.remain -= n
return n, err
}
if to.firstResponse {
// type + ver + lensize + 91 = 96
// type + ver + lensize + 1 = 6
// type + ver = 3
to.firstResponse = false
return to.read(b, 105)
}
// type + ver = 3
return to.read(b, 3)
}
func (to *TLSObfs) Write(b []byte) (int, error) {
if to.firstRequest {
helloMsg := makeClientHelloMsg(b, to.server)
_, err := to.Conn.Write(helloMsg)
to.firstRequest = false
return len(b), err
}
size := bufPool.Get().([]byte)
binary.BigEndian.PutUint16(size[:2], uint16(len(b)))
buf := &bytes.Buffer{}
buf.Write([]byte{0x17, 0x03, 0x03})
buf.Write(size[:2])
buf.Write(b)
_, err := to.Conn.Write(buf.Bytes())
bufPool.Put(size[:cap(size)])
return len(b), err
}
// NewTLSObfs return a SimpleObfs
func NewTLSObfs(conn net.Conn, server string) net.Conn {
return &TLSObfs{
Conn: conn,
server: server,
firstRequest: true,
firstResponse: true,
}
}
func makeClientHelloMsg(data []byte, server string) []byte {
random := make([]byte, 32)
sessionID := make([]byte, 32)
size := make([]byte, 2)
rand.Read(random)
rand.Read(sessionID)
buf := &bytes.Buffer{}
// handshake, TLS 1.0 version, length
buf.WriteByte(22)
buf.Write([]byte{0x03, 0x01})
length := uint16(212 + len(data) + len(server))
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length & 0xff))
// clientHello, length, TLS 1.2 version
buf.WriteByte(1)
binary.BigEndian.PutUint16(size, uint16(208+len(data)+len(server)))
buf.WriteByte(0)
buf.Write(size)
buf.Write([]byte{0x03, 0x03})
// random, sid len, sid
buf.Write(random)
buf.WriteByte(32)
buf.Write(sessionID)
// cipher suites
buf.Write([]byte{0x00, 0x38})
buf.Write([]byte{
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
})
// compression
buf.Write([]byte{0x01, 0x00})
// extension length
binary.BigEndian.PutUint16(size, uint16(79+len(data)+len(server)))
buf.Write(size)
// session ticket
buf.Write([]byte{0x00, 0x23})
binary.BigEndian.PutUint16(size, uint16(len(data)))
buf.Write(size)
buf.Write(data)
// server name
buf.Write([]byte{0x00, 0x00})
binary.BigEndian.PutUint16(size, uint16(len(server)+5))
buf.Write(size)
binary.BigEndian.PutUint16(size, uint16(len(server)+3))
buf.Write(size)
buf.WriteByte(0)
binary.BigEndian.PutUint16(size, uint16(len(server)))
buf.Write(size)
buf.Write([]byte(server))
// ec_point
buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02})
// groups
buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
// signature
buf.Write([]byte{
0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05,
0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01,
0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
})
// encrypt then mac
buf.Write([]byte{0x00, 0x16, 0x00, 0x00})
// extended master secret
buf.Write([]byte{0x00, 0x17, 0x00, 0x00})
return buf.Bytes()
}

View File

@ -1,115 +0,0 @@
package vmess
import (
"crypto/cipher"
"encoding/binary"
"errors"
"io"
)
type aeadWriter struct {
io.Writer
cipher.AEAD
nonce [32]byte
count uint16
iv []byte
}
func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
return &aeadWriter{Writer: w, AEAD: aead, iv: iv}
}
func (w *aeadWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
break
}
readLen := chunkSize - w.Overhead()
if length < readLen {
readLen = length
}
payloadBuf := buf[lenSize : lenSize+chunkSize-w.Overhead()]
copy(payloadBuf, b[n:n+readLen])
binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen+w.Overhead()))
binary.BigEndian.PutUint16(w.nonce[:2], w.count)
copy(w.nonce[2:], w.iv[2:12])
w.Seal(payloadBuf[:0], w.nonce[:w.NonceSize()], payloadBuf[:readLen], nil)
w.count++
_, err = w.Writer.Write(buf[:lenSize+readLen+w.Overhead()])
if err != nil {
break
}
n += readLen
length -= readLen
}
return
}
type aeadReader struct {
io.Reader
cipher.AEAD
nonce [32]byte
buf []byte
offset int
iv []byte
sizeBuf []byte
count uint16
}
func newAEADReader(r io.Reader, aead cipher.AEAD, iv []byte) *aeadReader {
return &aeadReader{Reader: r, AEAD: aead, iv: iv, sizeBuf: make([]byte, lenSize)}
}
func (r *aeadReader) Read(b []byte) (int, error) {
if r.buf != nil {
n := copy(b, r.buf[r.offset:])
r.offset += n
if r.offset == len(r.buf) {
bufPool.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
return n, nil
}
_, err := io.ReadFull(r.Reader, r.sizeBuf)
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(r.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
}
buf := bufPool.Get().([]byte)
_, err = io.ReadFull(r.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
binary.BigEndian.PutUint16(r.nonce[:2], r.count)
copy(r.nonce[2:], r.iv[2:12])
_, err = r.Open(buf[:0], r.nonce[:r.NonceSize()], buf[:size], nil)
r.count++
if err != nil {
return 0, err
}
realLen := size - r.Overhead()
n := copy(b, buf[:realLen])
if len(b) >= realLen {
bufPool.Put(buf[:cap(buf)])
return n, nil
}
r.offset = n
r.buf = buf[:realLen]
return n, nil
}

View File

@ -1,103 +0,0 @@
package vmess
import (
"encoding/binary"
"errors"
"io"
"sync"
)
const (
lenSize = 2
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead()
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, maxSize) }}
type chunkReader struct {
io.Reader
buf []byte
sizeBuf []byte
offset int
}
func newChunkReader(reader io.Reader) *chunkReader {
return &chunkReader{Reader: reader, sizeBuf: make([]byte, lenSize)}
}
func newChunkWriter(writer io.WriteCloser) *chunkWriter {
return &chunkWriter{Writer: writer}
}
func (cr *chunkReader) Read(b []byte) (int, error) {
if cr.buf != nil {
n := copy(b, cr.buf[cr.offset:])
cr.offset += n
if cr.offset == len(cr.buf) {
bufPool.Put(cr.buf[:cap(cr.buf)])
cr.buf = nil
}
return n, nil
}
_, err := io.ReadFull(cr.Reader, cr.sizeBuf)
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(cr.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
}
if len(b) >= size {
_, err := io.ReadFull(cr.Reader, b[:size])
if err != nil {
return 0, err
}
return size, nil
}
buf := bufPool.Get().([]byte)
_, err = io.ReadFull(cr.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
n := copy(b, cr.buf[:])
cr.offset = n
cr.buf = buf[:size]
return n, nil
}
type chunkWriter struct {
io.Writer
}
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
break
}
readLen := chunkSize
if length < chunkSize {
readLen = length
}
payloadBuf := buf[lenSize : lenSize+chunkSize]
copy(payloadBuf, b[n:n+readLen])
binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen))
_, err = cw.Writer.Write(buf[:lenSize+readLen])
if err != nil {
break
}
n += readLen
length -= readLen
}
return
}

View File

@ -1,213 +0,0 @@
package vmess
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"encoding/binary"
"errors"
"hash/fnv"
"io"
"math/rand"
"net"
"time"
"golang.org/x/crypto/chacha20poly1305"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// Conn wrapper a net.Conn with vmess protocol
type Conn struct {
net.Conn
reader io.Reader
writer io.Writer
dst *DstAddr
id *ID
reqBodyIV []byte
reqBodyKey []byte
respBodyIV []byte
respBodyKey []byte
respV byte
security byte
sent bool
received bool
}
func (vc *Conn) Write(b []byte) (int, error) {
if vc.sent {
return vc.writer.Write(b)
}
if err := vc.sendRequest(); err != nil {
return 0, err
}
vc.sent = true
return vc.writer.Write(b)
}
func (vc *Conn) Read(b []byte) (int, error) {
if vc.received {
return vc.reader.Read(b)
}
if err := vc.recvResponse(); err != nil {
return 0, err
}
vc.received = true
return vc.reader.Read(b)
}
func (vc *Conn) sendRequest() error {
timestamp := make([]byte, 8)
binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UTC().Unix()))
h := hmac.New(md5.New, vc.id.UUID.Bytes())
h.Write(timestamp)
_, err := vc.Conn.Write(h.Sum(nil))
if err != nil {
return err
}
buf := &bytes.Buffer{}
// Ver IV Key V Opt
buf.WriteByte(Version)
buf.Write(vc.reqBodyIV[:])
buf.Write(vc.reqBodyKey[:])
buf.WriteByte(vc.respV)
buf.WriteByte(OptionChunkStream)
p := rand.Intn(16)
// P Sec Reserve Cmd
buf.WriteByte(byte(p<<4) | byte(vc.security))
buf.WriteByte(0)
buf.WriteByte(CommandTCP)
// Port AddrType Addr
binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port))
buf.WriteByte(vc.dst.AddrType)
buf.Write(vc.dst.Addr)
// padding
if p > 0 {
padding := make([]byte, p)
rand.Read(padding)
buf.Write(padding)
}
fnv1a := fnv.New32a()
fnv1a.Write(buf.Bytes())
buf.Write(fnv1a.Sum(nil))
block, err := aes.NewCipher(vc.id.CmdKey)
if err != nil {
return err
}
stream := cipher.NewCFBEncrypter(block, hashTimestamp(time.Now().UTC()))
stream.XORKeyStream(buf.Bytes(), buf.Bytes())
_, err = vc.Conn.Write(buf.Bytes())
return err
}
func (vc *Conn) recvResponse() error {
block, err := aes.NewCipher(vc.respBodyKey[:])
if err != nil {
return err
}
stream := cipher.NewCFBDecrypter(block, vc.respBodyIV[:])
buf := make([]byte, 4)
_, err = io.ReadFull(vc.Conn, buf)
if err != nil {
return err
}
stream.XORKeyStream(buf, buf)
if buf[0] != vc.respV {
return errors.New("unexpected response header")
}
if buf[2] != 0 {
return errors.New("dynamic port is not supported now")
}
return nil
}
func hashTimestamp(t time.Time) []byte {
md5hash := md5.New()
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(t.UTC().Unix()))
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
return md5hash.Sum(nil)
}
// newConn return a Conn instance
func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) *Conn {
randBytes := make([]byte, 33)
rand.Read(randBytes)
reqBodyIV := make([]byte, 16)
reqBodyKey := make([]byte, 16)
copy(reqBodyIV[:], randBytes[:16])
copy(reqBodyKey[:], randBytes[16:32])
respV := randBytes[32]
respBodyKey := md5.Sum(reqBodyKey[:])
respBodyIV := md5.Sum(reqBodyIV[:])
var writer io.Writer
var reader io.Reader
switch security {
case SecurityNone:
reader = newChunkReader(conn)
writer = newChunkWriter(conn)
case SecurityAES128GCM:
block, _ := aes.NewCipher(reqBodyKey[:])
aead, _ := cipher.NewGCM(block)
writer = newAEADWriter(conn, aead, reqBodyIV[:])
block, _ = aes.NewCipher(respBodyKey[:])
aead, _ = cipher.NewGCM(block)
reader = newAEADReader(conn, aead, respBodyIV[:])
case SecurityCHACHA20POLY1305:
key := make([]byte, 32)
t := md5.Sum(reqBodyKey[:])
copy(key, t[:])
t = md5.Sum(key[:16])
copy(key[16:], t[:])
aead, _ := chacha20poly1305.New(key)
writer = newAEADWriter(conn, aead, reqBodyIV[:])
t = md5.Sum(respBodyKey[:])
copy(key, t[:])
t = md5.Sum(key[:16])
copy(key[16:], t[:])
aead, _ = chacha20poly1305.New(key)
reader = newAEADReader(conn, aead, respBodyIV[:])
}
return &Conn{
Conn: conn,
id: id,
dst: dst,
reqBodyIV: reqBodyIV,
reqBodyKey: reqBodyKey,
respV: respV,
respBodyIV: respBodyIV[:],
respBodyKey: respBodyKey[:],
reader: reader,
writer: writer,
security: security,
}
}

View File

@ -1,54 +0,0 @@
package vmess
import (
"bytes"
"crypto/md5"
"github.com/gofrs/uuid"
)
// ID cmdKey length
const (
IDBytesLen = 16
)
// The ID of en entity, in the form of a UUID.
type ID struct {
UUID *uuid.UUID
CmdKey []byte
}
// newID returns an ID with given UUID.
func newID(uuid *uuid.UUID) *ID {
id := &ID{UUID: uuid, CmdKey: make([]byte, IDBytesLen)}
md5hash := md5.New()
md5hash.Write(uuid.Bytes())
md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
md5hash.Sum(id.CmdKey[:0])
return id
}
func nextID(u *uuid.UUID) *uuid.UUID {
md5hash := md5.New()
md5hash.Write(u.Bytes())
md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81"))
var newid uuid.UUID
for {
md5hash.Sum(newid[:0])
if !bytes.Equal(newid.Bytes(), u.Bytes()) {
return &newid
}
md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2"))
}
}
func newAlterIDs(primary *ID, alterIDCount uint16) []*ID {
alterIDs := make([]*ID, alterIDCount)
prevID := primary.UUID
for idx := range alterIDs {
newid := nextID(prevID)
alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]}
prevID = newid
}
return alterIDs
}

View File

@ -1,117 +0,0 @@
package vmess
import (
"crypto/tls"
"fmt"
"math/rand"
"net"
"runtime"
"github.com/gofrs/uuid"
)
// Version of vmess
const Version byte = 1
// Request Options
const (
OptionChunkStream byte = 1
OptionChunkMasking byte = 4
)
// Security type vmess
type Security = byte
// Cipher types
const (
SecurityAES128GCM Security = 3
SecurityCHACHA20POLY1305 Security = 4
SecurityNone Security = 5
)
// CipherMapping return
var CipherMapping = map[string]byte{
"none": SecurityNone,
"aes-128-gcm": SecurityAES128GCM,
"chacha20-poly1305": SecurityCHACHA20POLY1305,
}
var tlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
// Command types
const (
CommandTCP byte = 1
CommandUDP byte = 2
)
// Addr types
const (
AtypIPv4 byte = 1
AtypDomainName byte = 2
AtypIPv6 byte = 3
)
// DstAddr store destination address
type DstAddr struct {
AddrType byte
Addr []byte
Port uint
}
// Client is vmess connection generator
type Client struct {
user []*ID
uuid *uuid.UUID
security Security
tls bool
}
// Config of vmess
type Config struct {
UUID string
AlterID uint16
Security string
TLS bool
}
// New return a Conn with net.Conn and DstAddr
func (c *Client) New(conn net.Conn, dst *DstAddr) net.Conn {
r := rand.Intn(len(c.user))
if c.tls {
conn = tls.Client(conn, tlsConfig)
}
return newConn(conn, c.user[r], dst, c.security)
}
// NewClient return Client instance
func NewClient(config Config) (*Client, error) {
uid, err := uuid.FromString(config.UUID)
if err != nil {
return nil, err
}
var security Security
switch config.Security {
case "aes-128-gcm":
security = SecurityAES128GCM
case "chacha20-poly1305":
security = SecurityCHACHA20POLY1305
case "none":
security = SecurityNone
case "auto":
security = SecurityCHACHA20POLY1305
if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" {
security = SecurityAES128GCM
}
default:
return nil, fmt.Errorf("Unknown security type: %s", config.Security)
}
return &Client{
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
tls: config.TLS,
}, nil
}

View File

@ -1,455 +0,0 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"strings"
"sync"
"time"
adapters "github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/common/observable"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
R "github.com/Dreamacro/clash/rules"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
var (
config *Config
once sync.Once
)
// General config
type General struct {
Port int
SocksPort int
RedirPort int
AllowLan bool
Mode Mode
LogLevel C.LogLevel
}
// ProxyConfig is update proxy schema
type ProxyConfig struct {
Port *int
SocksPort *int
RedirPort *int
AllowLan *bool
}
// RawConfig is raw config struct
type RawConfig struct {
Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"`
AllowLan bool `yaml:"allow-lan"`
Mode string `yaml:"mode"`
LogLevel string `yaml:"log-level"`
ExternalController string `yaml:"external-controller"`
Secret string `yaml:"secret"`
Proxy []map[string]interface{} `yaml:"Proxy"`
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
Rule []string `yaml:"Rule"`
}
// Config is clash config manager
type Config struct {
general *General
rules []C.Rule
proxies map[string]C.Proxy
lastUpdate time.Time
event chan<- interface{}
reportCh chan interface{}
observable *observable.Observable
}
// Event is event of clash config
type Event struct {
Type string
Payload interface{}
}
// Subscribe config stream
func (c *Config) Subscribe() observable.Subscription {
sub, _ := c.observable.Subscribe()
return sub
}
// Report return a channel for collecting report message
func (c *Config) Report() chan<- interface{} {
return c.reportCh
}
func (c *Config) readConfig() (*RawConfig, error) {
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
return nil, err
}
data, err := ioutil.ReadFile(C.Path.Config())
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("Configuration file %s is empty", C.Path.Config())
}
// config with some default value
rawConfig := &RawConfig{
AllowLan: false,
Mode: Rule.String(),
LogLevel: C.INFO.String(),
Rule: []string{},
Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{},
}
err = yaml.Unmarshal([]byte(data), &rawConfig)
return rawConfig, err
}
// Parse config
func (c *Config) Parse() error {
cfg, err := c.readConfig()
if err != nil {
return err
}
if err := c.parseGeneral(cfg); err != nil {
return err
}
if err := c.parseProxies(cfg); err != nil {
return err
}
return c.parseRules(cfg)
}
// Proxies return proxies of clash
func (c *Config) Proxies() map[string]C.Proxy {
return c.proxies
}
// Rules return rules of clash
func (c *Config) Rules() []C.Rule {
return c.rules
}
// SetMode change mode of clash
func (c *Config) SetMode(mode Mode) {
c.general.Mode = mode
c.event <- &Event{Type: "mode", Payload: mode}
}
// SetLogLevel change log level of clash
func (c *Config) SetLogLevel(level C.LogLevel) {
c.general.LogLevel = level
c.event <- &Event{Type: "log-level", Payload: level}
}
// General return clash general config
func (c *Config) General() General {
return *c.general
}
// UpdateRules is a function for hot reload rules
func (c *Config) UpdateRules() error {
cfg, err := c.readConfig()
if err != nil {
return err
}
return c.parseRules(cfg)
}
func (c *Config) parseGeneral(cfg *RawConfig) error {
port := cfg.Port
socksPort := cfg.SocksPort
redirPort := cfg.RedirPort
allowLan := cfg.AllowLan
logLevelString := cfg.LogLevel
modeString := cfg.Mode
mode, exist := ModeMapping[modeString]
if !exist {
return fmt.Errorf("General.mode value invalid")
}
logLevel, exist := C.LogLevelMapping[logLevelString]
if !exist {
return fmt.Errorf("General.log-level value invalid")
}
c.general = &General{
Port: port,
SocksPort: socksPort,
RedirPort: redirPort,
AllowLan: allowLan,
Mode: mode,
LogLevel: logLevel,
}
if restAddr := cfg.ExternalController; restAddr != "" {
c.event <- &Event{Type: "external-controller", Payload: restAddr}
c.event <- &Event{Type: "secret", Payload: cfg.Secret}
}
c.UpdateGeneral(*c.general)
return nil
}
// UpdateGeneral dispatch update event
func (c *Config) UpdateGeneral(general General) {
c.UpdateProxy(ProxyConfig{
Port: &general.Port,
SocksPort: &general.SocksPort,
RedirPort: &general.RedirPort,
AllowLan: &general.AllowLan,
})
c.event <- &Event{Type: "mode", Payload: general.Mode}
c.event <- &Event{Type: "log-level", Payload: general.LogLevel}
}
// UpdateProxy dispatch update proxy event
func (c *Config) UpdateProxy(pc ProxyConfig) {
if pc.AllowLan != nil {
c.general.AllowLan = *pc.AllowLan
}
c.general.Port = *or(pc.Port, &c.general.Port)
if c.general.Port != 0 && (pc.AllowLan != nil || pc.Port != nil) {
c.event <- &Event{Type: "http-addr", Payload: genAddr(c.general.Port, c.general.AllowLan)}
}
c.general.SocksPort = *or(pc.SocksPort, &c.general.SocksPort)
if c.general.SocksPort != 0 && (pc.AllowLan != nil || pc.SocksPort != nil) {
c.event <- &Event{Type: "socks-addr", Payload: genAddr(c.general.SocksPort, c.general.AllowLan)}
}
c.general.RedirPort = *or(pc.RedirPort, &c.general.RedirPort)
if c.general.RedirPort != 0 && (pc.AllowLan != nil || pc.RedirPort != nil) {
c.event <- &Event{Type: "redir-addr", Payload: genAddr(c.general.RedirPort, c.general.AllowLan)}
}
}
func (c *Config) parseProxies(cfg *RawConfig) error {
proxies := make(map[string]C.Proxy)
proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxies["DIRECT"] = adapters.NewDirect()
proxies["REJECT"] = adapters.NewReject()
// parse proxy
for idx, mapping := range proxiesConfig {
proxyType, existType := mapping["type"].(string)
proxyName, existName := mapping["name"].(string)
if !existType && existName {
return fmt.Errorf("Proxy %d missing type or name", idx)
}
if _, exist := proxies[proxyName]; exist {
return fmt.Errorf("Proxy %s is the duplicate name", proxyName)
}
var proxy C.Proxy
var err error
switch proxyType {
case "ss":
ssOption := &adapters.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption)
if err != nil {
break
}
proxy, err = adapters.NewShadowSocks(*ssOption)
case "socks5":
socksOption := &adapters.Socks5Option{}
err = decoder.Decode(mapping, socksOption)
if err != nil {
break
}
proxy = adapters.NewSocks5(*socksOption)
case "vmess":
vmessOption := &adapters.VmessOption{}
err = decoder.Decode(mapping, vmessOption)
if err != nil {
break
}
proxy, err = adapters.NewVmess(*vmessOption)
default:
return fmt.Errorf("Unsupport proxy type: %s", proxyType)
}
if err != nil {
return fmt.Errorf("Proxy %s: %s", proxyName, err.Error())
}
proxies[proxyName] = proxy
}
// parse proxy group
for idx, mapping := range groupsConfig {
groupType, existType := mapping["type"].(string)
groupName, existName := mapping["name"].(string)
if !existType && existName {
return fmt.Errorf("ProxyGroup %d: missing type or name", idx)
}
if _, exist := proxies[groupName]; exist {
return fmt.Errorf("ProxyGroup %s: the duplicate name", groupName)
}
var group C.Proxy
var err error
switch groupType {
case "url-test":
urlTestOption := &adapters.URLTestOption{}
err = decoder.Decode(mapping, urlTestOption)
if err != nil {
break
}
var ps []C.Proxy
for _, name := range urlTestOption.Proxies {
p, ok := proxies[name]
if !ok {
return fmt.Errorf("ProxyGroup %s: proxy or proxy group '%s' not found", groupName, name)
}
ps = append(ps, p)
}
group, err = adapters.NewURLTest(*urlTestOption, ps)
case "select":
selectorOption := &adapters.SelectorOption{}
err = decoder.Decode(mapping, selectorOption)
if err != nil {
break
}
selectProxy := make(map[string]C.Proxy)
for _, name := range selectorOption.Proxies {
proxy, exist := proxies[name]
if !exist {
return fmt.Errorf("ProxyGroup %s: proxy or proxy group '%s' not found", groupName, name)
}
selectProxy[name] = proxy
}
group, err = adapters.NewSelector(selectorOption.Name, selectProxy)
case "fallback":
fallbackOption := &adapters.FallbackOption{}
err = decoder.Decode(mapping, fallbackOption)
if err != nil {
break
}
var ps []C.Proxy
for _, name := range fallbackOption.Proxies {
p, ok := proxies[name]
if !ok {
return fmt.Errorf("ProxyGroup %s: proxy or proxy group '%s' not found", groupName, name)
}
ps = append(ps, p)
}
group, err = adapters.NewFallback(*fallbackOption, ps)
}
if err != nil {
return fmt.Errorf("Proxy %s: %s", groupName, err.Error())
}
proxies[groupName] = group
}
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", proxies)
// close old goroutine
for _, proxy := range c.proxies {
switch raw := proxy.(type) {
case *adapters.URLTest:
raw.Close()
case *adapters.Fallback:
raw.Close()
}
}
c.proxies = proxies
c.event <- &Event{Type: "proxies", Payload: proxies}
return nil
}
func (c *Config) parseRules(cfg *RawConfig) error {
rules := []C.Rule{}
rulesConfig := cfg.Rule
// parse rules
for _, line := range rulesConfig {
rule := strings.Split(line, ",")
if len(rule) < 3 {
continue
}
rule = trimArr(rule)
switch rule[0] {
case "DOMAIN":
rules = append(rules, R.NewDomain(rule[1], rule[2]))
case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
case "FINAL":
rules = append(rules, R.NewFinal(rule[2]))
}
}
c.rules = rules
c.event <- &Event{Type: "rules", Payload: rules}
return nil
}
func (c *Config) handleResponseMessage() {
for elm := range c.reportCh {
event := elm.(*Event)
switch event.Type {
case "http-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening HTTP proxy at %d error", c.general.Port)
c.general.Port = 0
}
case "socks-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening SOCKS proxy at %d error", c.general.SocksPort)
c.general.SocksPort = 0
}
case "redir-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening Redir proxy at %d error", c.general.RedirPort)
c.general.RedirPort = 0
}
}
}
}
func newConfig() *Config {
event := make(chan interface{})
reportCh := make(chan interface{})
config := &Config{
general: &General{},
proxies: make(map[string]C.Proxy),
rules: []C.Rule{},
lastUpdate: time.Now(),
event: event,
reportCh: reportCh,
observable: observable.NewObservable(event),
}
go config.handleResponseMessage()
return config
}
// Instance return singleton instance of Config
func Instance() *Config {
once.Do(func() {
config = newConfig()
})
return config
}

View File

@ -1,79 +0,0 @@
package config
import (
"archive/tar"
"compress/gzip"
"io"
"net/http"
"os"
"strings"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
func downloadMMDB(path string) (err error) {
resp, err := http.Get("http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz")
if err != nil {
return
}
defer resp.Body.Close()
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
h, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return err
}
if !strings.HasSuffix(h.Name, "GeoLite2-Country.mmdb") {
continue
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
if err != nil {
return err
}
}
return nil
}
// Init prepare necessary files
func Init() {
// initial homedir
if _, err := os.Stat(C.Path.HomeDir()); os.IsNotExist(err) {
if err := os.MkdirAll(C.Path.HomeDir(), 0777); err != nil {
log.Fatalf("Can't create config directory %s: %s", C.Path.HomeDir(), err.Error())
}
}
// initial config.ini
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
log.Info("Can't find config, create a empty file")
os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644)
}
// initial mmdb
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Info("Can't find MMDB, start download")
err := downloadMMDB(C.Path.MMDB())
if err != nil {
log.Fatalf("Can't download MMDB: %s", err.Error())
}
}
}

View File

@ -1,31 +0,0 @@
package config
type Mode int
var (
// ModeMapping is a mapping for Mode enum
ModeMapping = map[string]Mode{
"Global": Global,
"Rule": Rule,
"Direct": Direct,
}
)
const (
Global Mode = iota
Rule
Direct
)
func (m Mode) String() string {
switch m {
case Global:
return "Global"
case Rule:
return "Rule"
case Direct:
return "Direct"
default:
return "Unknow"
}
}

View File

@ -1,29 +0,0 @@
package config
import (
"fmt"
"strings"
)
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
func genAddr(port int, allowLan bool) string {
if allowLan {
return fmt.Sprintf(":%d", port)
}
return fmt.Sprintf("127.0.0.1:%d", port)
}
func or(pointers ...*int) *int {
for _, p := range pointers {
if p != nil {
return p
}
}
return pointers[len(pointers)-1]
}

View File

@ -1,59 +0,0 @@
package constant
import (
"net"
)
// Adapter Type
const (
Direct AdapterType = iota
Fallback
Reject
Selector
Shadowsocks
Socks5
URLTest
Vmess
)
type ProxyAdapter interface {
Conn() net.Conn
Close()
}
type ServerAdapter interface {
Metadata() *Metadata
Close()
}
type Proxy interface {
Name() string
Type() AdapterType
Generator(metadata *Metadata) (ProxyAdapter, error)
}
// AdapterType is enum of adapter type
type AdapterType int
func (at AdapterType) String() string {
switch at {
case Direct:
return "Direct"
case Fallback:
return "Fallback"
case Reject:
return "Reject"
case Selector:
return "Selector"
case Shadowsocks:
return "Shadowsocks"
case Socks5:
return "Socks5"
case URLTest:
return "URLTest"
case Vmess:
return "Vmess"
default:
return "Unknow"
}
}

View File

@ -1,10 +0,0 @@
package constant
type General struct {
Mode *string `json:"mode,omitempty"`
AllowLan *bool `json:"allow-lan,omitempty"`
Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port,omitempty"`
RedirPort *int `json:"redir-port,omitempty"`
LogLevel *string `json:"log-level,omitempty"`
}

View File

@ -1,35 +0,0 @@
package constant
var (
// LogLevelMapping is a mapping for LogLevel enum
LogLevelMapping = map[string]LogLevel{
"error": ERROR,
"warning": WARNING,
"info": INFO,
"debug": DEBUG,
}
)
const (
ERROR LogLevel = iota
WARNING
INFO
DEBUG
)
type LogLevel int
func (l LogLevel) String() string {
switch l {
case INFO:
return "info"
case WARNING:
return "warning"
case ERROR:
return "error"
case DEBUG:
return "debug"
default:
return "unknow"
}
}

View File

@ -1,46 +0,0 @@
package constant
import (
"net"
)
// Socks addr type
const (
AtypIPv4 = 1
AtypDomainName = 3
AtypIPv6 = 4
TCP NetWork = iota
UDP
HTTP SourceType = iota
SOCKS
)
type NetWork int
func (n *NetWork) String() string {
if *n == TCP {
return "tcp"
}
return "udp"
}
type SourceType int
// Metadata is used to store connection address
type Metadata struct {
NetWork NetWork
Source SourceType
AddrType int
Host string
IP *net.IP
Port string
}
func (addr *Metadata) String() string {
if addr.Host == "" {
return addr.IP.String()
}
return addr.Host
}

View File

@ -1,49 +0,0 @@
package constant
import (
"os"
"os/user"
P "path"
)
const Name = "clash"
// Path is used to get the configuration path
var Path *path
type path struct {
homedir string
}
func init() {
currentUser, err := user.Current()
var homedir string
if err != nil {
dir := os.Getenv("HOME")
if dir == "" {
dir, _ = os.Getwd()
}
homedir = dir
} else {
homedir = currentUser.HomeDir
}
homedir = P.Join(homedir, ".config", Name)
Path = &path{homedir: homedir}
}
// SetHomeDir is used to set the configuration path
func SetHomeDir(root string) {
Path = &path{homedir: root}
}
func (p *path) HomeDir() string {
return p.homedir
}
func (p *path) Config() string {
return P.Join(p.homedir, "config.yml")
}
func (p *path) MMDB() string {
return P.Join(p.homedir, "Country.mmdb")
}

View File

@ -1,7 +0,0 @@
package constant
// ProxySignal is used to handle graceful shutdown of proxy
type ProxySignal struct {
Done chan<- struct{}
Closed <-chan struct{}
}

View File

@ -1,39 +0,0 @@
package constant
// Rule Type
const (
Domain RuleType = iota
DomainSuffix
DomainKeyword
GEOIP
IPCIDR
FINAL
)
type RuleType int
func (rt RuleType) String() string {
switch rt {
case Domain:
return "Domain"
case DomainSuffix:
return "DomainSuffix"
case DomainKeyword:
return "DomainKeyword"
case GEOIP:
return "GEOIP"
case IPCIDR:
return "IPCIDR"
case FINAL:
return "FINAL"
default:
return "Unknow"
}
}
type Rule interface {
RuleType() RuleType
IsMatch(metadata *Metadata) bool
Adapter() string
Payload() string
}

View File

@ -1,55 +0,0 @@
package constant
import (
"time"
)
type Traffic struct {
up chan int64
down chan int64
upCount int64
downCount int64
upTotal int64
downTotal int64
interval time.Duration
}
func (t *Traffic) Up() chan<- int64 {
return t.up
}
func (t *Traffic) Down() chan<- int64 {
return t.down
}
func (t *Traffic) Now() (up int64, down int64) {
return t.upTotal, t.downTotal
}
func (t *Traffic) handle() {
go t.handleCh(t.up, &t.upCount, &t.upTotal)
go t.handleCh(t.down, &t.downCount, &t.downTotal)
}
func (t *Traffic) handleCh(ch <-chan int64, count *int64, total *int64) {
ticker := time.NewTicker(t.interval)
for {
select {
case n := <-ch:
*count += n
case <-ticker.C:
*total = *count
*count = 0
}
}
}
func NewTraffic(interval time.Duration) *Traffic {
t := &Traffic{
up: make(chan int64),
down: make(chan int64),
interval: interval,
}
go t.handle()
return t
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

44
examples/basic.py Normal file
View File

@ -0,0 +1,44 @@
import asyncio
from mihomo import Language, MihomoAPI
from mihomo.models import StarrailInfoParsed
from mihomo.models.v1 import StarrailInfoParsedV1
client = MihomoAPI(language=Language.EN)
async def v1():
data: StarrailInfoParsedV1 = await client.fetch_user_v1(800333171)
print(f"Name: {data.player.name}")
print(f"Level: {data.player.level}")
print(f"Signature: {data.player.signature}")
print(f"Achievements: {data.player_details.achievements}")
print(f"Characters count: {data.player_details.characters}")
print(f"Profile picture url: {client.get_icon_url(data.player.icon)}")
for character in data.characters:
print("-----------")
print(f"Name: {character.name}")
print(f"Rarity: {character.rarity}")
print(f"Level: {character.level}")
print(f"Avatar url: {client.get_icon_url(character.icon)}")
print(f"Preview url: {client.get_icon_url(character.preview)}")
print(f"Portrait url: {client.get_icon_url(character.portrait)}")
async def v2():
data: StarrailInfoParsed = await client.fetch_user(800333171, replace_icon_name_with_url=True)
print(f"Name: {data.player.name}")
print(f"Level: {data.player.level}")
print(f"Signature: {data.player.signature}")
print(f"Profile picture url: {data.player.avatar.icon}")
for character in data.characters:
print("-----------")
print(f"Name: {character.name}")
print(f"Rarity: {character.rarity}")
print(f"Portrait url: {character.portrait}")
asyncio.run(v1())
asyncio.run(v2())

View File

@ -0,0 +1,25 @@
import asyncio
import pickle
import zlib
from mihomo import Language, MihomoAPI, StarrailInfoParsed
async def main():
client = MihomoAPI(language=Language.EN)
data = await client.fetch_user(800333171)
# Save
pickle_data = zlib.compress(pickle.dumps(data))
print(len(pickle_data))
json_data = data.json(by_alias=True, ensure_ascii=False)
print(len(json_data))
# Load
data_from_pickle = pickle.loads(zlib.decompress(pickle_data))
data_from_json = StarrailInfoParsed.parse_raw(json_data)
print(type(data_from_pickle))
print(type(data_from_json))
asyncio.run(main())

19
examples/merge_data.py Normal file
View File

@ -0,0 +1,19 @@
import asyncio
from mihomo import Language, MihomoAPI, tools
async def main():
client = MihomoAPI(language=Language.EN)
old_data = await client.fetch_user(800333171)
# Change characters in game and wait for the API to refresh
# ...
new_data = await client.fetch_user(800333171)
data = tools.merge_character_data(new_data, old_data)
print(data)
asyncio.run(main())

16
go.mod
View File

@ -1,16 +0,0 @@
module github.com/Dreamacro/clash
require (
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181016063207-89bf7cffdaf4
github.com/eapache/queue v1.1.0 // indirect
github.com/go-chi/chi v3.3.3+incompatible
github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1
github.com/gofrs/uuid v3.1.0+incompatible
github.com/oschwald/geoip2-golang v1.2.1
github.com/oschwald/maxminddb-golang v1.3.0 // indirect
github.com/sirupsen/logrus v1.1.0
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941
gopkg.in/eapache/channels.v1 v1.1.0
gopkg.in/yaml.v2 v2.2.1
)

41
go.sum
View File

@ -1,41 +0,0 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181016063207-89bf7cffdaf4 h1:n+F4LoHdFCwsGohVN+4dwfmmx0h2uvdPEeiwdhFeH8g=
github.com/Dreamacro/go-shadowsocks2 v0.1.2-0.20181016063207-89bf7cffdaf4/go.mod h1:DlkXRxmh5K+99aTPQaVjsZ1fAZNFw42vXGcOjR3Otps=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
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/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8=
github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
github.com/oschwald/maxminddb-golang v1.3.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
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/sirupsen/logrus v1.1.0 h1:65VZabgUiV9ktjGM5nTq0+YurgTyX+YI2lSSfDjI+qU=
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941 h1:qBTHLajHecfu+xzRI9PqVDcqx7SdHj9d4B+EzSn3tAc=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e h1:EfdBzeKbFSvOjoIqSZcfS8wp0FBLokGBEs9lz1OtSg0=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4=
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,33 +0,0 @@
package hub
import (
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
)
var (
tunnel = T.Instance()
cfg = config.Instance()
listener = proxy.Instance()
)
type Error struct {
Error string `json:"error"`
}
type Errors struct {
Errors map[string]string `json:"errors"`
}
func formatErrors(errorsMap map[string]error) (bool, Errors) {
errors := make(map[string]string)
hasError := false
for key, err := range errorsMap {
if err != nil {
errors[key] = err.Error()
hasError = true
}
}
return hasError, Errors{Errors: errors}
}

View File

@ -1,96 +0,0 @@
package hub
import (
"fmt"
"net/http"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func configRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs)
r.Put("/", updateConfigs)
return r
}
type configSchema struct {
Port int `json:"port"`
SocksPort int `json:"socket-port"`
RedirPort int `json:"redir-port"`
AllowLan bool `json:"allow-lan"`
Mode string `json:"mode"`
LogLevel string `json:"log-level"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
general := cfg.General()
render.JSON(w, r, configSchema{
Port: general.Port,
SocksPort: general.SocksPort,
RedirPort: general.RedirPort,
AllowLan: general.AllowLan,
Mode: general.Mode.String(),
LogLevel: general.LogLevel.String(),
})
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
general := &C.General{}
err := render.DecodeJSON(r.Body, general)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
// update errors
var modeErr, logLevelErr error
// update mode
if general.Mode != nil {
mode, ok := config.ModeMapping[*general.Mode]
if !ok {
modeErr = fmt.Errorf("Mode error")
} else {
cfg.SetMode(mode)
}
}
// update log-level
if general.LogLevel != nil {
level, ok := C.LogLevelMapping[*general.LogLevel]
if !ok {
logLevelErr = fmt.Errorf("Log Level error")
} else {
cfg.SetLogLevel(level)
}
}
hasError, errors := formatErrors(map[string]error{
"mode": modeErr,
"log-level": logLevelErr,
})
if hasError {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, errors)
return
}
// update proxy
cfg.UpdateProxy(config.ProxyConfig{
AllowLan: general.AllowLan,
Port: general.Port,
SocksPort: general.SocksPort,
RedirPort: general.RedirPort,
})
w.WriteHeader(http.StatusNoContent)
}

View File

@ -1,203 +0,0 @@
package hub
import (
"fmt"
"net/http"
"strconv"
"time"
A "github.com/Dreamacro/clash/adapters/outbound"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies)
r.Get("/{name}", getProxy)
r.Get("/{name}/delay", getProxyDelay)
r.Put("/{name}", updateProxy)
return r
}
type SampleProxy struct {
Type string `json:"type"`
}
type Selector struct {
Type string `json:"type"`
Now string `json:"now"`
All []string `json:"all"`
}
type URLTest struct {
Type string `json:"type"`
Now string `json:"now"`
}
type Fallback struct {
Type string `json:"type"`
Now string `json:"now"`
}
func transformProxy(proxy C.Proxy) interface{} {
t := proxy.Type()
switch t {
case C.Selector:
selector := proxy.(*A.Selector)
return Selector{
Type: t.String(),
Now: selector.Now(),
All: selector.All(),
}
case C.URLTest:
return URLTest{
Type: t.String(),
Now: proxy.(*A.URLTest).Now(),
}
case C.Fallback:
return Fallback{
Type: t.String(),
Now: proxy.(*A.Fallback).Now(),
}
default:
return SampleProxy{
Type: proxy.Type().String(),
}
}
}
type GetProxiesResponse struct {
Proxies map[string]interface{} `json:"proxies"`
}
func getProxies(w http.ResponseWriter, r *http.Request) {
rawProxies := cfg.Proxies()
proxies := make(map[string]interface{})
for name, proxy := range rawProxies {
proxies[name] = transformProxy(proxy)
}
render.JSON(w, r, GetProxiesResponse{Proxies: proxies})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
render.JSON(w, r, transformProxy(proxy))
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := chi.URLParam(r, "name")
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Proxy can't update",
})
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: fmt.Sprintf("Selector update error: %s", err.Error()),
})
return
}
w.WriteHeader(http.StatusNoContent)
}
type GetProxyDelayRequest struct {
URL string `json:"url"`
Timeout int16 `json:"timeout"`
}
type GetProxyDelayResponse struct {
Delay int16 `json:"delay"`
}
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
url := query.Get("url")
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := chi.URLParam(r, "name")
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
sigCh := make(chan int16)
go func() {
t, err := A.DelayTest(proxy, url)
if err != nil {
sigCh <- 0
}
sigCh <- t
}()
select {
case <-time.After(time.Millisecond * time.Duration(timeout)):
w.WriteHeader(http.StatusRequestTimeout)
render.JSON(w, r, Error{
Error: "Proxy delay test timeout",
})
case t := <-sigCh:
if t == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
render.JSON(w, r, Error{
Error: "An error occurred in the delay test",
})
} else {
render.JSON(w, r, GetProxyDelayResponse{
Delay: t,
})
}
}
}

View File

@ -1,55 +0,0 @@
package hub
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func ruleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRules)
r.Put("/", updateRules)
return r
}
type Rule struct {
Name string `json:"name"`
Payload string `json:"type"`
Proxy string `json:"proxy"`
}
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
func getRules(w http.ResponseWriter, r *http.Request) {
rawRules := cfg.Rules()
var rules []Rule
for _, rule := range rawRules {
rules = append(rules, Rule{
Name: rule.RuleType().String(),
Payload: rule.Payload(),
Proxy: rule.Adapter(),
})
}
w.WriteHeader(http.StatusOK)
render.JSON(w, r, GetRulesResponse{
Rules: rules,
})
}
func updateRules(w http.ResponseWriter, r *http.Request) {
err := cfg.UpdateRules()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -1,172 +0,0 @@
package hub
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
)
var secret = ""
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
func newHub(signal chan struct{}) {
var addr string
ch := config.Instance().Subscribe()
signal <- struct{}{}
count := 0
for {
elm := <-ch
event := elm.(*config.Event)
switch event.Type {
case "external-controller":
addr = event.Payload.(string)
count++
case "secret":
secret = event.Payload.(string)
count++
}
if count == 2 {
break
}
}
r := chi.NewRouter()
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
MaxAge: 300,
})
r.Use(cors.Handler, authentication)
r.With(jsonContentType).Get("/traffic", traffic)
r.With(jsonContentType).Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
log.Infof("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
if err != nil {
log.Errorf("External controller error: %s", err.Error())
}
}
func jsonContentType(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func authentication(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
text := strings.SplitN(header, " ", 2)
if secret == "" {
next.ServeHTTP(w, r)
return
}
hasUnvalidHeader := text[0] != "Bearer"
hasUnvalidSecret := len(text) == 2 && text[1] != secret
if hasUnvalidHeader || hasUnvalidSecret {
w.WriteHeader(http.StatusUnauthorized)
render.JSON(w, r, Error{
Error: "Authentication failed",
})
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
tick := time.NewTicker(time.Second)
t := tunnel.Traffic()
for range tick.C {
up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{
Up: up,
Down: down,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(w http.ResponseWriter, r *http.Request) {
levelText := r.URL.Query().Get("level")
if levelText == "" {
levelText = "info"
}
level, ok := C.LogLevelMapping[levelText]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Level error",
})
return
}
src := tunnel.Log()
sub, err := src.Subscribe()
defer src.UnSubscribe(sub)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
render.Status(r, http.StatusOK)
for elm := range sub {
log := elm.(T.Log)
if log.LogLevel > level {
continue
}
if err := json.NewEncoder(w).Encode(Log{
Type: log.Type(),
Payload: log.Payload,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
// Run initial hub
func Run() {
signal := make(chan struct{})
go newHub(signal)
<-signal
}

50
main.go
View File

@ -1,50 +0,0 @@
package main
import (
"os"
"os/signal"
"syscall"
"flag"
"path"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub"
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
var (
homedir string
)
func init() {
flag.StringVar(&homedir, "d", "", "set configuration directory")
flag.Parse()
}
func main() {
tunnel.Instance().Run()
proxy.Instance().Run()
hub.Run()
if (homedir != "") {
if !path.IsAbs(homedir) {
currentDir, _ := os.Getwd()
homedir = path.Join(currentDir, homedir)
}
C.SetHomeDir(homedir)
}
config.Init()
err := config.Instance().Parse()
if err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
}

4
mihomo/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from . import tools
from .client import *
from .errors import *
from .models import *

156
mihomo/client.py Normal file
View File

@ -0,0 +1,156 @@
import typing
from enum import Enum
import aiohttp
from . import tools
from .errors import HttpRequestError, InvalidParams, UserNotFound
from .models import StarrailInfoParsed
from .models.v1 import StarrailInfoParsedV1
class Language(Enum):
CHT = "cht"
CHS = "cn"
DE = "de"
EN = "en"
ES = "es"
FR = "fr"
ID = "id"
JP = "jp"
KR = "kr"
PT = "pt"
RU = "ru"
TH = "th"
VI = "vi"
class MihomoAPI:
"""
Represents an client for Mihomo API.
Args:
language (Language, optional):
The language to use for API responses.Defaults to Language.CHT.
Attributes:
- BASE_URL (str): The base URL of the API.
- ASSET_URL (str): The base URL for the asset files.
"""
BASE_URL: typing.Final[str] = "https://api.mihomo.me/sr_info_parsed"
ASSET_URL: typing.Final[
str
] = "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master"
def __init__(self, language: Language = Language.CHT):
self.lang = language
async def request(
self,
uid: int | str,
language: Language,
*,
params: dict[str, str] = {},
) -> typing.Any:
"""
Makes an HTTP request to the API.
Args:
- uid (int | str): The user ID.
- language (Language): The language to use for the API response.
Returns:
typing.Any: The response from the API.
Raises:
HttpRequestError: If the HTTP request fails.
InvalidParams: If the API request contains invalid parameters.
UserNotFound: If the requested user is not found.
"""
url = self.BASE_URL + "/" + str(uid)
params.update({"lang": language.value})
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response:
match response.status:
case 200:
return await response.json(encoding="utf-8")
case 400:
try:
data = await response.json(encoding="utf-8")
except:
raise InvalidParams()
else:
if isinstance(data, dict) and (
detail := data.get("detail")
):
raise InvalidParams(detail)
raise InvalidParams()
case 404:
raise UserNotFound()
case _:
raise HttpRequestError(response.status, str(response.reason))
async def fetch_user(
self,
uid: int,
*,
replace_icon_name_with_url: bool = False,
) -> StarrailInfoParsed:
"""
Fetches user data from the API.
Args:
- uid (`int`): The user ID.
- replace_icon_name_with_url (`bool`): Whether to replace icon names with asset URLs.
Returns:
StarrailInfoParsed: The parsed user data from mihomo API.
"""
data = await self.request(uid, self.lang)
if replace_icon_name_with_url is True:
data = tools.replace_icon_name_with_url(data)
data = StarrailInfoParsed.parse_obj(data)
return data
async def fetch_user_v1(
self,
uid: int,
*,
replace_icon_name_with_url: bool = False,
) -> StarrailInfoParsedV1:
"""
Fetches user data from the API using version 1 format.
Args:
- uid (`int`): The user ID.
- replace_icon_name_with_url (`bool`): Whether to replace icon names with asset URLs.
Returns:
StarrailInfoParsedV1: The parsed user data from the Mihomo API (version 1).
"""
data = await self.request(uid, self.lang, params={"version": "v1"})
data = tools.remove_empty_dict(data)
if replace_icon_name_with_url is True:
data = tools.replace_icon_name_with_url(data)
data = StarrailInfoParsedV1.parse_obj(data)
data = tools.replace_trailblazer_name(data)
return data
def get_icon_url(self, icon: str) -> str:
"""
Gets the asset url for the given icon.
Args:
icon (str): The icon name.
Returns:
str: The asset url for the icon.
"""
return self.ASSET_URL + "/" + icon

42
mihomo/errors.py Normal file
View File

@ -0,0 +1,42 @@
class BaseException(Exception):
"""Base exception class."""
message: str = ""
def __init__(self, message: str | None = None, *args: object) -> None:
if message is not None:
self.message = message
super().__init__(self.message, *args)
class HttpRequestError(BaseException):
"""Exception raised when an HTTP request fails."""
status: int = 0
reason: str = ""
def __init__(
self,
status: int,
reason: str,
message: str | None = None,
*args: object,
) -> None:
if not message:
message = f"[{status}] {reason}"
self.status = status
self.reason = reason
self.message = message
super().__init__(message, *args)
class UserNotFound(BaseException):
"""Exception raised when a user is not found."""
message = "User not found."
class InvalidParams(BaseException):
"""Exception raised when invalid parameters are provided."""
message: str = "Invalid parameters"

View File

@ -0,0 +1,6 @@
from .base import *
from .character import *
from .combat import *
from .equipment import *
from .player import *
from .stat import *

19
mihomo/models/base.py Normal file
View File

@ -0,0 +1,19 @@
from pydantic import BaseModel, Field
from .character import Character
from .player import Player
class StarrailInfoParsed(BaseModel):
"""
Mihomo parsed data
Attributes:
- player (`Player`): The player's info.
- characters (list[`Character`]): The list of characters.
"""
player: Player
"""Player's basic info"""
characters: list[Character]
"""The list of characters"""

View File

@ -0,0 +1,92 @@
from typing import Any
from pydantic import BaseModel, Field, root_validator
from .combat import Element, Path, Trace, TraceTreeNode
from .equipment import LightCone, Relic, RelicSet
from .stat import Attribute, Property
class Character(BaseModel):
"""
Represents a character.
Attributes:
- Basic info:
- id (`str`): The character's ID.
- name (`str`): The character's name.
- rarity (`int`): The character's rarity.
- level (`int`): The character's current level.
- max_level (`int`): The maximum character level according to the current ascension phase.
- ascension (`int`): Ascension phase.
- eidolon (`int`): The character's eidolon rank.
- eidolon_icons (list[`str`]): The list of character's eiodolon icons.
- Image
- icon (`str`): The character avatar image
- preview (`str`): The character's preview image.
- portrait (`str`): The character's portrait image.
- Combat
- path (`Path`): The character's path.
- element (`Element`): The character's element.
- traces (list[`Trace`]): The list of character's skill traces.
- trace_tree (list[`TraceTreeNode]): The list of the character's skill traces.
- Equipment
- light_cone (`LightCone` | `None`): The character's light cone (weapon), or None if not applicable.
- relics (list[`Relic`] | `None`): The list of character's relics, or None if not applicable.
- relic_set (list[`RelicSet`] | `None`): The list of character's relic sets, or None if not applicable.
- stats (list[`Stat`]): The list of character's stats.
- Stats
- attributes (list[`Attribute`]): The list of character's attributes.
- additions (list[`Attribute`]): The list of character's additional attributes.
- properties (list[`Property`]): The list of character's properties.
"""
id: str
"""Character's ID"""
name: str
"""Character's name"""
rarity: int
"""Character's rarity"""
level: int
"""Character's level"""
ascension: int = Field(..., alias="promotion")
"""Ascension phase"""
eidolon: int = Field(..., alias="rank")
"""Character's eidolon rank"""
eidolon_icons: list[str] = Field([], alias="rank_icons")
"""The list of character's eiodolon icons"""
icon: str
"""Character avatar image"""
preview: str
"""Character preview image"""
portrait: str
"""Character portrait image"""
path: Path
"""Character's path"""
element: Element
"""Character's element"""
traces: list[Trace] = Field(..., alias="skills")
"""The list of character's skill traces"""
trace_tree: list[TraceTreeNode] = Field([], alias="skill_trees")
"""The list of the character's skill traces"""
light_cone: LightCone | None = None
"""Character's light cone (weapon)"""
relics: list[Relic] = []
"""The list of character's relics"""
relic_sets: list[RelicSet] = []
"""The list of character's relic sets"""
attributes: list[Attribute]
"""The list of character's attributes"""
additions: list[Attribute]
"""The list of character's additional attributes"""
properties: list[Property]
"""The list of character's properties"""
@property
def max_level(self) -> int:
"""The maximum character level according to the current ascension phase"""
return 20 + 10 * self.ascension

112
mihomo/models/combat.py Normal file
View File

@ -0,0 +1,112 @@
from pydantic import BaseModel
class Element(BaseModel):
"""
Represents an element.
Attributes:
- id (`str`): The ID of the element.
- name (`str`): The name of the element.
- color (`str`): The color of the element.
- icon (`str`): The element icon.
"""
id: str
"""The ID of the element"""
name: str
"""The name of the element"""
color: str
"""The color of the element"""
icon: str
"""The element icon"""
class Path(BaseModel):
"""
Paths are congregations of Imaginary energy, with which the ideals harmonize.
Attributes:
- id (`str`): The ID of the path.
- name (`str`): The name of the path.
- icon (`str`): The path icon.
"""
id: str
"""The ID of the path"""
name: str
"""The name of the path"""
icon: str
"""The path icon"""
class Trace(BaseModel):
"""
Represents a character's skill trace.
Attributes:
- id (`int`): The ID of the trace.
- name (`str`): The name of the trace.
- level (`int`): The current level of the trace.
- max_level (`int`): The maximum level of the trace.
- element (`Element` | None): The element of the trace, or None if not applicable.
- type (`str`): The type of the trace.
- type_text (`str`): The type text of the trace.
- effect (`str`): The effect of the trace.
- effect_text (`str`): The effect text of the trace.
- simple_desc (`str`): The simple description of the trace.
- desc (`str`): The detailed description of the trace.
- icon (`str`): The trace icon.
"""
id: int
"""The ID of the trace"""
name: str
"""The name of the trace"""
level: int
"""The current level of the trace"""
max_level: int
"""The maximum level of the trace"""
element: Element | None = None
"""The element of the trace"""
type: str
"""The type of the trace"""
type_text: str
"""The type text of the trace"""
effect: str
"""The effect of the trace"""
effect_text: str
"""The effect text of the trace"""
simple_desc: str
"""The simple description of the trace"""
desc: str
"""The detailed description of the trace"""
icon: str
"""The trace icon"""
class TraceTreeNode(BaseModel):
"""
Represents a node in the trace skill tree of a character.
Attributes:
- id (`int`): The ID of the trace.
- level (`int`): The level of the trace.
- max_level (`int`): The max level of the trace.
- icon (`str`): The icon of the trace.
- anchor (`str`): The position of the trace tree node.
- parent (`int` | `None`): The preceding node id of trace.
"""
id: int
"""The ID of the trace"""
level: int
"""The level of the trace"""
max_level: int
"""The max level of the trace"""
icon: str
"""The icon of the trace"""
anchor: str
"""The position of the trace tree node"""
parent: int | None = None
"""The preceding node id of trace"""

118
mihomo/models/equipment.py Normal file
View File

@ -0,0 +1,118 @@
from pydantic import BaseModel, Field
from .combat import Path
from .stat import Attribute, MainAffix, Property, SubAffix
class LightCone(BaseModel):
"""
Represents a light cone (weapon).
Attributes:
- id (`int`): The ID of the light cone.
- name (`str`): The name of the light cone.
- rarity (`int`): The rarity of the light cone.
- superimpose (`int`): The superimpose rank of the light cone.
- level (`int`): The current level of the light cone.
- max_level (`int`): The maximum light cone level according to the current ascension phase.
- ascension (`int`): The ascension phase of the light cone.
- icon (`str`): The light cone icon image.
- preview (`str`): The light cone preview image.
- portrait (`str`): The light cone portrait image.
- path (`Path`): The path of the light cone.
- attributes (list[`Attribute`]): The list of attributes of the light cone.
- properties (list[`Property`]): The list of properties of the light cone.
"""
id: int
"""The ID of the light cone"""
name: str
"""The name of the light cone"""
rarity: int
"""The rarity of the light cone"""
superimpose: int = Field(..., alias="rank")
"""The superimpose rank of the light cone"""
level: int
"""The level of the light cone"""
ascension: int = Field(..., alias="promotion")
"""The ascension phase of the light cone"""
icon: str
"""The light cone icon image"""
preview: str
"""The light cone preview image"""
portrait: str
"""The light cone portrait image"""
path: Path
"""The path of the light cone"""
attributes: list[Attribute]
"""The list of attributes of the light cone"""
properties: list[Property]
"""The list of properties of the light cone"""
@property
def max_level(self) -> int:
"""The maximum light cone level according to the current ascension phase"""
return 20 + 10 * self.ascension
class Relic(BaseModel):
"""
Represents a relic.
Attributes:
- id (`int`): The ID of the relic.
- name (`str`): The name of the relic.
- set_id (`int`): The ID of the relic set.
- set_name (`str`): The name of the relic set.
- rarity (`int`): The rarity of the relic.
- level (`int`): The level of the relic.
- main_property (`MainAffix`): The main affix of the relic.
- sub_property (list[`SubAffix`]): The list of sub-affixes of the relic.
- icon (`str`): The relic icon.
"""
id: int
"""The ID of the relic"""
name: str
"""The name of the relic"""
set_id: int
"""The ID of the relic set"""
set_name: str
"""The name of the relic set"""
rarity: int
"""The rarity of the relic"""
level: int
"""The level of the relic"""
main_affix: MainAffix
"""The main affix of the relic"""
sub_affixes: list[SubAffix] = Field([], alias="sub_affix")
"""The list of sub-affixes of the relic"""
icon: str
"""The relic icon"""
class RelicSet(BaseModel):
"""
Represents a set of relics.
Attributes:
- id (`int`): The ID of the relic set.
- name (`str`): The name of the relic set.
- icon (`str`): The icon of the relic set.
- num (`int`): The number of relics in the set.
- desc (`str`): The description of the relic set.
- properties (list[`Property`]): The list of properties of the relic set.
"""
id: int
"""The ID of the relic set"""
name: str
"""The name of the relic set"""
icon: str
"""The icon of the relic set"""
num: int
"""The number of relics in the set"""
desc: str
"""The description of the relic set"""
properties: list[Property]
"""The list of properties of the relic set"""

97
mihomo/models/player.py Normal file
View File

@ -0,0 +1,97 @@
from pydantic import BaseModel, Field, root_validator
class Avatar(BaseModel):
"""Profile picture"""
id: int
name: str
icon: str
class ForgottenHall(BaseModel):
"""The progress of the Forgotten Hall
Attributes:
- memory (`int`): The progress of the memory.
- memory_of_chaos_id (`int`): The ID of the memory of chaos, or None if not applicable.
- memory_of_chaos (`int`): The progress of the memory of chaos, or None if not applicable.
"""
memory: int = Field(..., alias="level")
"""The progress of the memory (level)"""
memory_of_chaos_id: int = Field(..., alias="chaos_id")
"""The ID of the memory of chaos (chaos_id)"""
memory_of_chaos: int = Field(..., alias="chaos_level")
"""The progress of the memory of chaos (chaos_level)"""
class Player(BaseModel):
"""
Player basic info
Attributes:
- uid (`int`): The player's uid.
- name (`str`): The player's nickname.
- level (`int`): The player's Trailblaze level.
- world_level (`int`): The player's Equilibrium level.
- friend_count (`int`): The number of friends.
- avatar (`Avatar`): The player's profile picture.
- signature (`str`): The player's bio.
- is_display (`bool`): Is the player's profile display enabled.
- forgotten_hall (`ForgottenHall` | None): The progress of the Forgotten Hall, or None if not applicable.
- simulated_universes (`int`): The number of simulated universes passed.
- light_cones (`int`): The number of light cones owned.
- characters (`int`): The number of characters owned.
- achievements (`int`): The number of achievements unlocked.
"""
uid: int
"""Player's uid"""
name: str = Field(..., alias="nickname")
"""Player's nickname"""
level: int
"""Trailblaze level"""
world_level: int
"""Equilibrium level"""
friend_count: int
"""Number of friends"""
avatar: Avatar
"""Profile picture"""
signature: str
"""Bio"""
is_display: bool
"""Is the player's profile display enabled."""
forgotten_hall: ForgottenHall | None = Field(None, alias="memory_data")
"""The progress of the Forgotten Hall (memory_data)"""
simulated_universes: int = Field(0, alias="universe_level")
"""Number of simulated universes passed (universe_level)"""
light_cones: int = Field(0, alias="light_cone_count")
"""Number of light cones owned"""
characters: int = Field(0, alias="avatar_count")
"""Number of characters owned"""
achievements: int = Field(0, alias="achievement_count")
"""Number of achievements unlocked"""
@root_validator(pre=True)
def decompose_space_info(cls, data):
if isinstance(data, dict):
space_info = data.get("space_info")
if isinstance(space_info, dict):
data.update(space_info)
return data
@root_validator(pre=True)
def transform_for_backward_compatibility(cls, data):
if isinstance(data, dict):
if "pass_area_progress" in data and "universe_level" not in data:
data["universe_level"] = data["pass_area_progress"]
if "challenge_data" in data and "memory_data" not in data:
c: dict[str, int] = data["challenge_data"]
data["memory_data"] = {}
data["memory_data"]["level"] = c.get("pre_maze_group_index")
data["memory_data"]["chaos_id"] = c.get("maze_group_id")
data["memory_data"]["chaos_level"] = c.get("maze_group_index")
return data

97
mihomo/models/stat.py Normal file
View File

@ -0,0 +1,97 @@
from pydantic import BaseModel, Field
class Attribute(BaseModel):
"""
Represents an attribute.
Attributes:
- field (`str`): The field of the attribute.
- name (`str`): The name of the attribute.
- icon (`str`): The attribute icon image.
- value (`float`): The value of the attribute.
- displayed_value (`str`): The displayed value of the attribute.
- is_percent (`bool`): Indicates if the value is in percentage.
"""
field: str
"""The field of the attribute"""
name: str
"""The name of the attribute"""
icon: str
"""The attribute icon image"""
value: float
"""The value of the attribute"""
displayed_value: str = Field(..., alias="display")
"""The displayed value of the attribute"""
is_percent: bool = Field(..., alias="percent")
"""Indicates if the value is in percentage"""
class Property(BaseModel):
"""
Represents a property.
Attributes:
- type (`str`): The type of the property.
- field (`str`): The field of the property.
- name (`str`): The name of the property.
- icon (`str`): The property icon image.
- value (`float`): The value of the property.
- displayed_value (`str`): The displayed value of the property.
- is_percent (`bool`): Indicates if the value is in percentage.
"""
type: str
"""The type of the property"""
field: str
"""The field of the property"""
name: str
"""The name of the property"""
icon: str
"""The property icon image"""
value: float
"""The value of the property"""
displayed_value: str = Field(..., alias="display")
"""The displayed value of the property"""
is_percent: bool = Field(..., alias="percent")
"""Indicates if the value is in percentage"""
class MainAffix(Property):
"""
Represents a relic main affix.
Attributes:
- type (`str`): The type of the affix.
- field (`str`): The field of the affix.
- name (`str`): The name of the affix.
- icon (`str`): The affix icon image.
- value (`float`): The value of the affix.
- displayed_value (`str`): The displayed value of the affix.
- is_percent (`bool`): Indicates if the value is in percentage.
"""
...
class SubAffix(MainAffix):
"""
Represents a relic sub-affix.
Attributes:
- type (`str`): The type of the affix.
- field (`str`): The field of the affix.
- name (`str`): The name of the affix.
- icon (`str`): The affix icon image.
- value (`float`): The value of the affix.
- displayed_value (`str`): The displayed value of the affix.
- is_percent (`bool`): Indicates if the value is in percentage.
- count (`int`): The upgrade times of the affix.
- step (`int`): The additional value of the affix.
"""
count: int
"""The upgrade times of the affix"""
step: int
"""The additional value of the affix"""

View File

@ -0,0 +1,4 @@
from .base import *
from .character import *
from .equipment import *
from .player import *

22
mihomo/models/v1/base.py Normal file
View File

@ -0,0 +1,22 @@
from pydantic import BaseModel, Field
from .character import Character
from .player import Player, PlayerSpaceInfo
class StarrailInfoParsedV1(BaseModel):
"""
Mihomo parsed data V1
Attributes:
- player (`Player`): The player's basic info.
- player_details (`PlayerSpaceInfo`): The player's details.
- characters (list[`Character`]): The list of characters.
"""
player: Player
"""Player's basic info"""
player_details: PlayerSpaceInfo = Field(..., alias="PlayerSpaceInfo")
"""Player's details"""
characters: list[Character]
"""The list of characters"""

View File

@ -0,0 +1,150 @@
from typing import Any
from pydantic import BaseModel, Field, root_validator
from .equipment import LightCone, Relic, RelicSet
class EidolonIcon(BaseModel):
"""
Represents an Eidolon icon.
Attributes:
- icon (`str`): The eidolon icon.
- unlock (`bool`): Indicates if the eidolon is unlocked.
"""
icon: str
"""The eidolon icon"""
unlock: bool
"""Indicates if the eidolon is unlocked"""
class Trace(BaseModel):
"""
Represents a character's skill trace.
Attributes:
- name (`str`): The name of the trace.
- level (`int`): The level of the trace.
- type (`str`): The type of the trace.
- icon (`str`): The trace icon.
"""
name: str
"""The name of the trace"""
level: int
"""The level of the trace"""
type: str
"""The type of the trace"""
icon: str
"""The trace icon"""
class Stat(BaseModel):
"""
Represents a character's stat.
Attributes:
- name (`str`): The name of the stat.
- base (`str`): The base value of the stat.
- addition (`str` | `None`): The additional value of the stat, or None if not applicable.
- icon (`str`): The stat icon.
"""
name: str
"""The name of the stat"""
base: str
"""The base value of the stat"""
addition: str | None = None
"""The additional value of the stat"""
icon: str
"""The stat icon"""
class Character(BaseModel):
"""
Represents a character.
Attributes:
- Basic info:
- id (`str`): The character's ID.
- name (`str`): The character's name.
- rarity (`int`): The character's rarity.
- level (`int`): The character's level.
- Eidolon
- eidolon (`int`): The character's eidolon rank.
- eidolon_icons (list[`EidolonIcon`]): The list of eidolon icons.
- Image
- icon (`str`): The character avatar image
- preview (`str`): The character's preview image.
- portrait (`str`): The character's portrait image.
- Combat type
- path (`str`): The character's path.
- path_icon (`str`): The character's path icon.
- element (`str`): The character's element.
- element_icon (`str`): The character's element icon.
- color (`str`): The character's element color.
- Equipment
- traces (list[`Trace`]): The list of character's skill traces.
- light_cone (`LightCone` | `None`): The character's light cone (weapon), or None if not applicable.
- relics (list[`Relic`] | `None`): The list of character's relics, or None if not applicable.
- relic_set (list[`RelicSet`] | `None`): The list of character's relic sets, or None if not applicable.
- stats (list[`Stat`]): The list of character's stats.
"""
id: str
"""Character's ID"""
name: str
"""Character's name"""
rarity: int
"""Character's rarity"""
level: int
"""Character's level"""
eidolon: int = Field(..., alias="rank")
"""Character's eidolon rank"""
eidolon_icons: list[EidolonIcon] = Field(..., alias="rank_icons")
"""The list of eidolon icons"""
preview: str
"""Character preview image"""
portrait: str
"""Character portrait image"""
path: str
"""Character's path"""
path_icon: str
"""Character's path icon"""
element: str
"""Character's element"""
element_icon: str
"""Character's element icon"""
color: str
"""Character's element color"""
traces: list[Trace] = Field(..., alias="skill")
"""The list of character's skill traces"""
light_cone: LightCone | None = None
"""Character's light cone (weapon)"""
relics: list[Relic] | None = Field(None, alias="relic")
"""The list of character's relics"""
relic_set: list[RelicSet] | None = None
"""The list of character's relic sets"""
stats: list[Stat] = Field(..., alias="property")
"""The list of character's stats"""
@root_validator(pre=True)
def dict_to_list(cls, data: dict[str, Any]):
# The keys of the original dict is not necessary, so remove them here.
if isinstance(data, dict) and data.get("relic") is not None:
if isinstance(data["relic"], dict):
data["relic"] = list(data["relic"].values())
return data
@property
def icon(self) -> str:
"""Character avatar image"""
return f"icon/character/{self.id}.png"

View File

@ -0,0 +1,71 @@
from pydantic import BaseModel, Field
class LightCone(BaseModel):
"""
Represents a light cone (weapon).
Attributes:
- name (`str`): The name of the light cone.
- rarity (`int`): The rarity of the light cone.
- superimpose (`int`): The superimpose rank of the light cone.
- level (`int`): The level of the light cone.
- icon (`str`): The light cone icon.
"""
name: str
rarity: int
superimpose: int = Field(..., alias="rank")
level: int
icon: str
class RelicProperty(BaseModel):
"""
Represents a property of a relic.
Attributes:
- name (`str`): The name of the relic property.
- value (`str`): The value of the relic property.
- icon (`str`): The property icon.
"""
name: str
value: str
icon: str
class Relic(BaseModel):
"""
Represents a relic.
Attributes:
- name (`str`): The name of the relic.
- rarity (`int`): The rarity of the relic.
- level (`int`): The level of the relic.
- main_property (`RelicProperty`): The main property of the relic.
- sub_property (list[`RelicProperty`]): The list of sub properties of the relic.
- icon (`str`): The relic icon.
"""
name: str
rarity: int
level: int
main_property: RelicProperty
sub_property: list[RelicProperty]
icon: str
class RelicSet(BaseModel):
"""
Represents a set of relics.
Attributes:
- name (`str`): The name of the relic set.
- icon (`str`): The relic set icon.
- desc (`int`): The description of the relic set.
"""
name: str
icon: str
desc: int

View File

@ -0,0 +1,65 @@
from pydantic import BaseModel, Field
class Player(BaseModel):
"""
Player basic info
Attributes:
- uid (`str`): The player's uid.
- name (`str`): The player's nickname.
- level (`int`): The player's Trailblaze level.
- icon (`str`): The player's profile picture.
- signature (`str`): The player's bio.
"""
uid: str
"""Player's uid"""
name: str
"""Player's nickname"""
level: int
"""Trailblaze level"""
icon: str
"""Profile picture"""
signature: str
"""Bio"""
class ForgottenHall(BaseModel):
"""The progress of the Forgotten Hall
Attributes:
- memory (`int`): The progress of the memory.
- memory_of_chaos_id (`int` | `None`): The ID of the memory of chaos, or None if not applicable.
- memory_of_chaos (`int` | `None`): The progress of the memory of chaos, or None if not applicable.
"""
memory: int | None = Field(None, alias="PreMazeGroupIndex")
"""The progress of the memory"""
memory_of_chaos_id: int | None = Field(None, alias="MazeGroupIndex")
"""The ID of the memory of chaos"""
memory_of_chaos: int | None = Field(None, alias="MazeGroupID")
"""The progress of the memory of chaos"""
class PlayerSpaceInfo(BaseModel):
"""Player details
Attributes:
- forgotten_hall (`ForgottenHall` | None): The progress of the Forgotten Hall, or None if not applicable.
- simulated_universes (`int`): The number of simulated universes passed.
- light_cones (`int`): The number of light cones owned.
- characters (`int`): The number of characters owned.
- achievements (`int`): The number of achievements unlocked.
"""
forgotten_hall: ForgottenHall | None = Field(None, alias="ChallengeData")
"""The progress of the Forgotten Hall"""
simulated_universes: int = Field(0, alias="PassAreaProgress")
"""Number of simulated universes passed"""
light_cones: int = Field(0, alias="LightConeCount")
"""Number of light cones owned"""
characters: int = Field(0, alias="AvatarCount")
"""Number of characters owned"""
achievements: int = Field(0, alias="AchievementCount")
"""Number of achievements unlocked"""

107
mihomo/tools.py Normal file
View File

@ -0,0 +1,107 @@
from typing import Final, TypeVar
from .models import Character, StarrailInfoParsed
from .models.v1 import Character, StarrailInfoParsedV1
RawData = TypeVar("RawData")
ParsedData = TypeVar("ParsedData", StarrailInfoParsed, StarrailInfoParsedV1)
ASSET_URL: Final[str] = "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master"
def remove_empty_dict(data: RawData) -> RawData:
"""
Recursively removes empty dictionaries from the given raw data.
Args:
- data (`RawData`): The input raw data.
Returns:
- `RawData`: The data with empty dictionaries removed.
"""
if isinstance(data, dict):
for key in data.keys():
data[key] = None if (data[key] == {}) else remove_empty_dict(data[key])
elif isinstance(data, list):
for i in range(len(data)):
data[i] = remove_empty_dict(data[i])
return data
def replace_icon_name_with_url(data: RawData) -> RawData:
"""
Replaces icon file names with asset URLs in the given raw data.
Example: Replace "/icon/avatar/1201.png" with
"https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/avatar/1201.png"
Args:
- data (`RawData`): The input raw data.
Returns:
- `RawData`: The data with icon file names replaced by asset URLs.
"""
if isinstance(data, dict):
for key in data.keys():
data[key] = replace_icon_name_with_url(data[key])
elif isinstance(data, list):
for i in range(len(data)):
data[i] = replace_icon_name_with_url(data[i])
elif isinstance(data, str):
if ".png" in data:
data = ASSET_URL + "/" + data
return data
def replace_trailblazer_name(data: StarrailInfoParsedV1) -> StarrailInfoParsedV1:
"""
Replaces the trailblazer name with the player's name.
Args:
- data (`StarrailInfoParsed`): The input StarrailInfoParsed data.
Returns:
- `StarrailInfoParsed`: The updated StarrailInfoParsed data.
"""
for i in range(len(data.characters)):
if data.characters[i].name == r"{NICKNAME}":
data.characters[i].name = data.player.name
return data
def remove_duplicate_character(data: ParsedData) -> ParsedData:
"""
Removes duplicate characters from the given StarrailInfoParsed data.
Args:
- data (`ParsedData`): The input StarrailInfoParsed data.
Returns:
- `ParsedData`: The updated StarrailInfoParsed data without duplicate characters.
"""
new_characters = []
characters_ids: set[str] = set()
for character in data.characters:
if character.id not in characters_ids:
new_characters.append(character)
characters_ids.add(character.id)
data.characters = new_characters
return data
def merge_character_data(new_data: ParsedData, old_data: ParsedData) -> ParsedData:
"""
Append the old data characters to the list of new data characters.
The player's info from the old data will be omitted/discarded.
Args:
- new_data (`ParsedData`): The new data to be merged.
- old_data (`ParsedData`): The old data to merge into.
Returns:
- `ParsedData`: The merged new data.
"""
for character in old_data.characters:
new_data.characters.append(character)
new_data = remove_duplicate_character(new_data)
return new_data

View File

@ -1,74 +0,0 @@
package http
import (
"bufio"
"net"
"net/http"
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewHttpProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("HTTP proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleConn(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleConn(conn net.Conn) {
br := bufio.NewReader(conn)
request, err := http.ReadRequest(br)
if err != nil {
conn.Close()
return
}
if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
return
}
tun.Add(adapters.NewHTTPS(request, conn))
return
}
tun.Add(adapters.NewHTTP(request, conn))
}

View File

@ -1,116 +0,0 @@
package proxy
import (
"sync"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/redir"
"github.com/Dreamacro/clash/proxy/socks"
)
var (
listener *Listener
once sync.Once
)
type Listener struct {
// signal for update
httpSignal *C.ProxySignal
socksSignal *C.ProxySignal
redirSignal *C.ProxySignal
}
func (l *Listener) updateHTTP(addr string) error {
if l.httpSignal != nil {
signal := l.httpSignal
signal.Done <- struct{}{}
<-signal.Closed
l.httpSignal = nil
}
signal, err := http.NewHttpProxy(addr)
if err != nil {
return err
}
l.httpSignal = signal
return nil
}
func (l *Listener) updateSocks(addr string) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
<-signal.Closed
l.socksSignal = nil
}
signal, err := socks.NewSocksProxy(addr)
if err != nil {
return err
}
l.socksSignal = signal
return nil
}
func (l *Listener) updateRedir(addr string) error {
if l.redirSignal != nil {
signal := l.redirSignal
signal.Done <- struct{}{}
<-signal.Closed
l.redirSignal = nil
}
signal, err := redir.NewRedirProxy(addr)
if err != nil {
return err
}
l.redirSignal = signal
return nil
}
func (l *Listener) process(signal chan<- struct{}) {
sub := config.Instance().Subscribe()
signal <- struct{}{}
reportCH := config.Instance().Report()
for elm := range sub {
event := elm.(*config.Event)
switch event.Type {
case "http-addr":
addr := event.Payload.(string)
err := l.updateHTTP(addr)
reportCH <- &config.Event{Type: "http-addr", Payload: err == nil}
case "socks-addr":
addr := event.Payload.(string)
err := l.updateSocks(addr)
reportCH <- &config.Event{Type: "socks-addr", Payload: err == nil}
case "redir-addr":
addr := event.Payload.(string)
err := l.updateRedir(addr)
reportCH <- &config.Event{Type: "redir-addr", Payload: err == nil}
}
}
}
// Run ensure config monitoring
func (l *Listener) Run() {
signal := make(chan struct{})
go l.process(signal)
<-signal
}
func newListener() *Listener {
return &Listener{}
}
// Instance return singleton instance of Listener
func Instance() *Listener {
once.Do(func() {
listener = newListener()
})
return listener
}

View File

@ -1,62 +0,0 @@
package redir
import (
"net"
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewRedirProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("Redir proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleRedir(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleRedir(conn net.Conn) {
target, err := parserPacket(conn)
if err != nil {
conn.Close()
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
}

View File

@ -1,58 +0,0 @@
package redir
import (
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parserPacket(c net.Conn) (socks.Addr, error) {
const (
PfInout = 0
PfIn = 1
PfOut = 2
IOCOut = 0x40000000
IOCIn = 0x80000000
IOCInOut = IOCIn | IOCOut
IOCPARMMask = 0x1FFF
LEN = 4*16 + 4*4 + 4*1
// #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num))
// #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t))
// #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook)
DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23
)
fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY)
if err != nil {
return nil, err
}
defer syscall.Close(fd)
nl := struct { // struct pfioc_natlook
saddr, daddr, rsaddr, rdaddr [16]byte
sxport, dxport, rsxport, rdxport [4]byte
af, proto, protoVariant, direction uint8
}{
af: syscall.AF_INET,
proto: syscall.IPPROTO_TCP,
direction: PfOut,
}
saddr := c.RemoteAddr().(*net.TCPAddr)
daddr := c.LocalAddr().(*net.TCPAddr)
copy(nl.saddr[:], saddr.IP)
copy(nl.daddr[:], daddr.IP)
nl.sxport[0], nl.sxport[1] = byte(saddr.Port>>8), byte(saddr.Port)
nl.dxport[0], nl.dxport[1] = byte(daddr.Port>>8), byte(daddr.Port)
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 {
return nil, errno
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
copy(addr[1:1+net.IPv4len], nl.rdaddr[:4])
copy(addr[1+net.IPv4len:], nl.rdxport[:2])
return addr, nil
}

View File

@ -1,51 +0,0 @@
package redir
import (
"errors"
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
const (
SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
c, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("only work with TCP connection")
}
rc, err := c.SyscallConn()
if err != nil {
return nil, err
}
var addr socks.Addr
rc.Control(func(fd uintptr) {
addr, err = getorigdst(fd)
})
return addr, err
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (socks.Addr, error) {
raw := syscall.RawSockaddrInet4{}
siz := unsafe.Sizeof(raw)
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
return nil, err
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
copy(addr[1:1+net.IPv4len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]
return addr, nil
}

View File

@ -1,17 +0,0 @@
package redir
import (
"syscall"
"unsafe"
)
const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
var a [6]uintptr
a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5
if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 {
return errno
}
return nil
}

View File

@ -1,14 +0,0 @@
// +build linux,!386
package redir
import "syscall"
const GETSOCKOPT = syscall.SYS_GETSOCKOPT
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 {
return errno
}
return nil
}

View File

@ -1,12 +0,0 @@
package redir
import (
"errors"
"net"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
return nil, errors.New("Windows not support yet")
}

View File

@ -1,63 +0,0 @@
package socks
import (
"net"
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
"github.com/Dreamacro/go-shadowsocks2/socks"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("SOCKS proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleSocks(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleSocks(conn net.Conn) {
target, err := socks.Handshake(conn)
if err != nil {
conn.Close()
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
}

View File

@ -1 +0,0 @@
package socks

22
pyproject.toml Normal file
View File

@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "mihomo"
version = "1.1.7"
authors = [
{ name="KT", email="xns77477@gmail.com" },
]
description = "A simple Python Pydantic model for Honkai: Star Rail parsed data from the Mihomo API."
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"aiohttp==3.*",
"pydantic==1.*",
]

View File

@ -1,36 +0,0 @@
package rules
import (
C "github.com/Dreamacro/clash/constant"
)
type Domain struct {
domain string
adapter string
}
func (d *Domain) RuleType() C.RuleType {
return C.Domain
}
func (d *Domain) IsMatch(metadata *C.Metadata) bool {
if metadata.AddrType != C.AtypDomainName {
return false
}
return metadata.Host == d.domain
}
func (d *Domain) Adapter() string {
return d.adapter
}
func (d *Domain) Payload() string {
return d.domain
}
func NewDomain(domain string, adapter string) *Domain {
return &Domain{
domain: domain,
adapter: adapter,
}
}

View File

@ -1,39 +0,0 @@
package rules
import (
"strings"
C "github.com/Dreamacro/clash/constant"
)
type DomainKeyword struct {
keyword string
adapter string
}
func (dk *DomainKeyword) RuleType() C.RuleType {
return C.DomainKeyword
}
func (dk *DomainKeyword) IsMatch(metadata *C.Metadata) bool {
if metadata.AddrType != C.AtypDomainName {
return false
}
domain := metadata.Host
return strings.Contains(domain, dk.keyword)
}
func (dk *DomainKeyword) Adapter() string {
return dk.adapter
}
func (dk *DomainKeyword) Payload() string {
return dk.keyword
}
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
return &DomainKeyword{
keyword: keyword,
adapter: adapter,
}
}

View File

@ -1,39 +0,0 @@
package rules
import (
"strings"
C "github.com/Dreamacro/clash/constant"
)
type DomainSuffix struct {
suffix string
adapter string
}
func (ds *DomainSuffix) RuleType() C.RuleType {
return C.DomainSuffix
}
func (ds *DomainSuffix) IsMatch(metadata *C.Metadata) bool {
if metadata.AddrType != C.AtypDomainName {
return false
}
domain := metadata.Host
return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix
}
func (ds *DomainSuffix) Adapter() string {
return ds.adapter
}
func (ds *DomainSuffix) Payload() string {
return ds.suffix
}
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
return &DomainSuffix{
suffix: suffix,
adapter: adapter,
}
}

View File

@ -1,31 +0,0 @@
package rules
import (
C "github.com/Dreamacro/clash/constant"
)
type Final struct {
adapter string
}
func (f *Final) RuleType() C.RuleType {
return C.FINAL
}
func (f *Final) IsMatch(metadata *C.Metadata) bool {
return true
}
func (f *Final) Adapter() string {
return f.adapter
}
func (f *Final) Payload() string {
return ""
}
func NewFinal(adapter string) *Final {
return &Final{
adapter: adapter,
}
}

View File

@ -1,54 +0,0 @@
package rules
import (
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/oschwald/geoip2-golang"
log "github.com/sirupsen/logrus"
)
var (
mmdb *geoip2.Reader
once sync.Once
)
type GEOIP struct {
country string
adapter string
}
func (g *GEOIP) RuleType() C.RuleType {
return C.GEOIP
}
func (g *GEOIP) IsMatch(metadata *C.Metadata) bool {
if metadata.IP == nil {
return false
}
record, _ := mmdb.Country(*metadata.IP)
return record.Country.IsoCode == g.country
}
func (g *GEOIP) Adapter() string {
return g.adapter
}
func (g *GEOIP) Payload() string {
return g.country
}
func NewGEOIP(country string, adapter string) *GEOIP {
once.Do(func() {
var err error
mmdb, err = geoip2.Open(C.Path.MMDB())
if err != nil {
log.Fatalf("Can't load mmdb: %s", err.Error())
}
})
return &GEOIP{
country: country,
adapter: adapter,
}
}

View File

@ -1,42 +0,0 @@
package rules
import (
"net"
C "github.com/Dreamacro/clash/constant"
)
type IPCIDR struct {
ipnet *net.IPNet
adapter string
}
func (i *IPCIDR) RuleType() C.RuleType {
return C.IPCIDR
}
func (i *IPCIDR) IsMatch(metadata *C.Metadata) bool {
if metadata.IP == nil {
return false
}
return i.ipnet.Contains(*metadata.IP)
}
func (i *IPCIDR) Adapter() string {
return i.adapter
}
func (i *IPCIDR) Payload() string {
return i.ipnet.String()
}
func NewIPCIDR(s string, adapter string) *IPCIDR {
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
}
return &IPCIDR{
ipnet: ipnet,
adapter: adapter,
}
}

View File

@ -1,77 +0,0 @@
package tunnel
import (
"bufio"
"io"
"net"
"net/http"
"time"
"github.com/Dreamacro/clash/adapters/inbound"
C "github.com/Dreamacro/clash/constant"
)
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
req := request.R
host := req.Host
for {
req.Header.Set("Connection", "close")
req.RequestURI = ""
adapters.RemoveHopByHopHeaders(req.Header)
err := req.Write(conn)
if err != nil {
break
}
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, req)
if err != nil {
break
}
adapters.RemoveHopByHopHeaders(resp.Header)
if resp.ContentLength >= 0 {
resp.Header.Set("Proxy-Connection", "keep-alive")
resp.Header.Set("Connection", "keep-alive")
resp.Header.Set("Keep-Alive", "timeout=4")
resp.Close = false
} else {
resp.Close = true
}
err = resp.Write(request.Conn())
if err != nil || resp.Close {
break
}
req, err = http.ReadRequest(bufio.NewReader(request.Conn()))
if err != nil {
break
}
// Sometimes firefox just open a socket to process multiple domains in HTTP
// The temporary solution is close connection when encountering different HOST
if req.Host != host {
break
}
}
}
func (t *Tunnel) handleSOCKS(request *adapters.SocketAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
relay(request.Conn(), conn)
}
// relay copies between left and right bidirectionally.
func relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
go func() {
_, err := io.Copy(leftConn, rightConn)
leftConn.SetReadDeadline(time.Now())
ch <- err
}()
io.Copy(rightConn, leftConn)
rightConn.SetReadDeadline(time.Now())
<-ch
}

View File

@ -1,51 +0,0 @@
package tunnel
import (
"fmt"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
type Log struct {
LogLevel C.LogLevel
Payload string
}
func (l *Log) Type() string {
return l.LogLevel.String()
}
func print(data Log) {
switch data.LogLevel {
case C.INFO:
log.Infoln(data.Payload)
case C.WARNING:
log.Warnln(data.Payload)
case C.ERROR:
log.Errorln(data.Payload)
case C.DEBUG:
log.Debugln(data.Payload)
}
}
func (t *Tunnel) subscribeLogs() {
sub, err := t.observable.Subscribe()
if err != nil {
log.Fatalf("Can't subscribe tunnel log: %s", err.Error())
}
for elm := range sub {
data := elm.(Log)
if data.LogLevel <= t.logLevel {
print(data)
}
}
}
func newLog(logLevel C.LogLevel, format string, v ...interface{}) Log {
return Log{
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),
}
}

View File

@ -1,161 +0,0 @@
package tunnel
import (
"sync"
"time"
InboundAdapter "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/observable"
cfg "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"gopkg.in/eapache/channels.v1"
)
var (
tunnel *Tunnel
once sync.Once
)
// Tunnel handle proxy socket and HTTP/SOCKS socket
type Tunnel struct {
queue *channels.InfiniteChannel
rules []C.Rule
proxies map[string]C.Proxy
configLock *sync.RWMutex
traffic *C.Traffic
// Outbound Rule
mode cfg.Mode
// Log
logCh chan interface{}
observable *observable.Observable
logLevel C.LogLevel
}
// Add request to queue
func (t *Tunnel) Add(req C.ServerAdapter) {
t.queue.In() <- req
}
// Traffic return traffic of all connections
func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic
}
// Log return clash log stream
func (t *Tunnel) Log() *observable.Observable {
return t.observable
}
func (t *Tunnel) configMonitor(signal chan<- struct{}) {
sub := cfg.Instance().Subscribe()
signal <- struct{}{}
for elm := range sub {
event := elm.(*cfg.Event)
switch event.Type {
case "proxies":
proxies := event.Payload.(map[string]C.Proxy)
t.configLock.Lock()
t.proxies = proxies
t.configLock.Unlock()
case "rules":
rules := event.Payload.([]C.Rule)
t.configLock.Lock()
t.rules = rules
t.configLock.Unlock()
case "mode":
t.mode = event.Payload.(cfg.Mode)
case "log-level":
t.logLevel = event.Payload.(C.LogLevel)
}
}
}
func (t *Tunnel) process() {
queue := t.queue.Out()
for {
elm := <-queue
conn := elm.(C.ServerAdapter)
go t.handleConn(conn)
}
}
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
defer localConn.Close()
metadata := localConn.Metadata()
var proxy C.Proxy
switch t.mode {
case cfg.Direct:
proxy = t.proxies["DIRECT"]
case cfg.Global:
proxy = t.proxies["GLOBAL"]
// Rule
default:
proxy = t.match(metadata)
}
remoConn, err := proxy.Generator(metadata)
if err != nil {
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error())
return
}
defer remoConn.Close()
switch adapter := localConn.(type) {
case *InboundAdapter.HTTPAdapter:
t.handleHTTP(adapter, remoConn)
case *InboundAdapter.SocketAdapter:
t.handleSOCKS(adapter, remoConn)
}
}
func (t *Tunnel) match(metadata *C.Metadata) C.Proxy {
t.configLock.RLock()
defer t.configLock.RUnlock()
for _, rule := range t.rules {
if rule.IsMatch(metadata) {
a, ok := t.proxies[rule.Adapter()]
if !ok {
continue
}
t.logCh <- newLog(C.INFO, "%v match %s using %s", metadata.String(), rule.RuleType().String(), rule.Adapter())
return a
}
}
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", metadata.String())
return t.proxies["DIRECT"]
}
// Run initial task
func (t *Tunnel) Run() {
go t.process()
go t.subscribeLogs()
signal := make(chan struct{})
go t.configMonitor(signal)
<-signal
}
func newTunnel() *Tunnel {
logCh := make(chan interface{})
return &Tunnel{
queue: channels.NewInfiniteChannel(),
proxies: make(map[string]C.Proxy),
observable: observable.NewObservable(logCh),
logCh: logCh,
configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second),
mode: cfg.Rule,
logLevel: C.INFO,
}
}
// Instance return singleton instance of Tunnel
func Instance() *Tunnel {
once.Do(func() {
tunnel = newTunnel()
})
return tunnel
}

View File

@ -1,29 +0,0 @@
package tunnel
import (
"net"
C "github.com/Dreamacro/clash/constant"
)
// TrafficTrack record traffic of net.Conn
type TrafficTrack struct {
net.Conn
traffic *C.Traffic
}
func (tt *TrafficTrack) Read(b []byte) (int, error) {
n, err := tt.Conn.Read(b)
tt.traffic.Down() <- int64(n)
return n, err
}
func (tt *TrafficTrack) Write(b []byte) (int, error) {
n, err := tt.Conn.Write(b)
tt.traffic.Up() <- int64(n)
return n, err
}
func newTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
return &TrafficTrack{traffic: traffic, Conn: conn}
}