mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-01-10 00:29:01 +08:00
Compare commits
No commits in common. "v0.6.0" and "main" have entirely different histories.
80
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
80
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
|
||||
80
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
Normal file
80
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
Normal 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
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
|
||||
23
.github/ISSUE_TEMPLATE/feature_request_zh.yml
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request_zh.yml
vendored
Normal 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
17
.github/workflows/Delete.yml
vendored
Normal 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
35
.github/workflows/build.yml
vendored
Normal 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
20
.gitignore
vendored
@ -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
|
||||
27
.travis.yml
27
.travis.yml
@ -1,27 +0,0 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- "1.10"
|
||||
before_install:
|
||||
- go get -u github.com/golang/dep/cmd/dep
|
||||
install:
|
||||
- "$GOPATH/bin/dep ensure"
|
||||
env:
|
||||
global:
|
||||
- NAME=clash
|
||||
- BINDIR=bin
|
||||
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
|
||||
19
Dockerfile
19
Dockerfile
@ -1,19 +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
|
||||
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh && \
|
||||
mkdir -p /go/src/github.com/Dreamacro/clash
|
||||
WORKDIR /go/src/github.com/Dreamacro/clash
|
||||
COPY . /go/src/github.com/Dreamacro/clash
|
||||
RUN dep ensure && \
|
||||
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"]
|
||||
103
Gopkg.lock
generated
103
Gopkg.lock
generated
@ -1,103 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Yawning/chacha20"
|
||||
packages = ["."]
|
||||
revision = "e3b1f968fc6397b51d963fee8ec8711a47bc0ce8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/eapache/queue"
|
||||
packages = ["."]
|
||||
revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-chi/chi"
|
||||
packages = ["."]
|
||||
revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35"
|
||||
version = "v3.3.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-chi/cors"
|
||||
packages = ["."]
|
||||
revision = "dba6525398619dead495962a916728e7ee2ca322"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-chi/render"
|
||||
packages = ["."]
|
||||
revision = "3215478343fbc559bd3fc08f7031bb134d6bdad5"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/oschwald/geoip2-golang"
|
||||
packages = ["."]
|
||||
revision = "7118115686e16b77967cdbf55d1b944fe14ad312"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/oschwald/maxminddb-golang"
|
||||
packages = ["."]
|
||||
revision = "c5bec84d1963260297932a1b7a1753c8420717a7"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/riobard/go-shadowsocks2"
|
||||
packages = [
|
||||
"core",
|
||||
"shadowaead",
|
||||
"shadowstream",
|
||||
"socks"
|
||||
]
|
||||
revision = "8346403248229fc7e10d7a259de8e9352a9d8830"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"chacha20poly1305",
|
||||
"hkdf",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"poly1305",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"cpu",
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/eapache/channels.v1"
|
||||
packages = ["."]
|
||||
revision = "47238d5aae8c0fefd518ef2bee46290909cf8263"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ini.v1"
|
||||
packages = ["."]
|
||||
revision = "358ee7663966325963d4e8b2e1fbd570c5195153"
|
||||
version = "v1.38.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "0ccb06b3617c87e75bd650f92adc99e55b93070e0b2a0bc71634270226e125fc"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
62
Gopkg.toml
62
Gopkg.toml
@ -1,62 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-chi/chi"
|
||||
version = "3.3.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-chi/cors"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-chi/render"
|
||||
version = "1.0.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/oschwald/geoip2-golang"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/riobard/go-shadowsocks2"
|
||||
version = "0.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.5"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/eapache/channels.v1"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ini.v1"
|
||||
version = "1.38.1"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
22
LICENSE
22
LICENSE
@ -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.
|
||||
23
Makefile
23
Makefile
@ -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)/*
|
||||
192
README.md
192
README.md
@ -1,117 +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
|
||||
|
||||
## 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
|
||||
|
||||
Configuration file at `$HOME/.config/clash/config.ini`
|
||||
|
||||
Below is a simple demo configuration file:
|
||||
|
||||
```ini
|
||||
[General]
|
||||
port = 7890
|
||||
socks-port = 7891
|
||||
|
||||
# redir proxy for Linux and macOS
|
||||
redir-port = 7892
|
||||
|
||||
# A RESTful API for clash
|
||||
external-controller = 127.0.0.1:8080
|
||||
|
||||
[Proxy]
|
||||
# name = ss, server, port, cipher, password
|
||||
# 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
|
||||
ss1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
|
||||
ss2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
|
||||
|
||||
# name = socks5, server, port
|
||||
socks = socks5, server1, port
|
||||
|
||||
[Proxy Group]
|
||||
# url-test select which proxy will be used by benchmarking speed to a URL.
|
||||
# name = url-test, [proxies], url, interval(second)
|
||||
auto = url-test, ss1, ss2, http://www.google.com/generate_204, 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 = select, [proxies]
|
||||
Proxy = select, ss1, ss2, auto
|
||||
|
||||
[Rule]
|
||||
DOMAIN-SUFFIX,google.com,Proxy
|
||||
DOMAIN-KEYWORD,google,Proxy
|
||||
DOMAIN-SUFFIX,ad.com,REJECT
|
||||
GEOIP,CN,DIRECT
|
||||
FINAL,,Proxy # note: there is two ","
|
||||
### Tools
|
||||
`from mihomo import tools`
|
||||
#### Remove Duplicate Character
|
||||
```py
|
||||
data = await client.fetch_user(800333171)
|
||||
data = tools.remove_duplicate_character(data)
|
||||
```
|
||||
|
||||
## Thanks
|
||||
#### Merge Character Data
|
||||
```py
|
||||
old_data = await client.fetch_user(800333171)
|
||||
|
||||
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||
# Change characters in game and wait for the API to refresh
|
||||
# ...
|
||||
|
||||
[google/tcpproxy](https://github.com/google/tcpproxy)
|
||||
new_data = await client.fetch_user(800333171)
|
||||
data = tools.merge_character_data(new_data, old_data)
|
||||
```
|
||||
|
||||
## License
|
||||
### Data Persistence
|
||||
Take pickle and json as an example
|
||||
```py
|
||||
import pickle
|
||||
import zlib
|
||||
from mihomo import MihomoAPI, Language, StarrailInfoParsed
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
||||
client = MihomoAPI(language=Language.EN)
|
||||
data = await client.fetch_user(800333171)
|
||||
|
||||
## TODO
|
||||
# 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))
|
||||
|
||||
- [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))
|
||||
```
|
||||
|
||||
@ -1,82 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
// PeekedConn handle http connection and buffed HTTP data
|
||||
type PeekedConn struct {
|
||||
net.Conn
|
||||
Peeked []byte
|
||||
host string
|
||||
isHTTP bool
|
||||
}
|
||||
|
||||
func (c *PeekedConn) Read(p []byte) (n int, err error) {
|
||||
if len(c.Peeked) > 0 {
|
||||
n = copy(p, c.Peeked)
|
||||
c.Peeked = c.Peeked[n:]
|
||||
if len(c.Peeked) == 0 {
|
||||
c.Peeked = nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Sometimes firefox just open a socket to process multiple domains in HTTP
|
||||
// The temporary solution is to return io.EOF when encountering different HOST
|
||||
if c.isHTTP {
|
||||
br := bufio.NewReader(bytes.NewReader(p))
|
||||
_, hostName := ParserHTTPHostHeader(br)
|
||||
if hostName != "" {
|
||||
if !strings.Contains(hostName, ":") {
|
||||
hostName += ":80"
|
||||
}
|
||||
|
||||
if hostName != c.host {
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
|
||||
// HTTPAdapter is a adapter for HTTP connection
|
||||
type HTTPAdapter struct {
|
||||
addr *C.Addr
|
||||
conn *PeekedConn
|
||||
}
|
||||
|
||||
// Close HTTP connection
|
||||
func (h *HTTPAdapter) Close() {
|
||||
h.conn.Close()
|
||||
}
|
||||
|
||||
// Addr return destination address
|
||||
func (h *HTTPAdapter) Addr() *C.Addr {
|
||||
return h.addr
|
||||
}
|
||||
|
||||
// Conn return raw net.Conn of HTTP
|
||||
func (h *HTTPAdapter) Conn() net.Conn {
|
||||
return h.conn
|
||||
}
|
||||
|
||||
// NewHTTP is HTTPAdapter generator
|
||||
func NewHTTP(host string, peeked []byte, isHTTP bool, conn net.Conn) *HTTPAdapter {
|
||||
return &HTTPAdapter{
|
||||
addr: parseHTTPAddr(host),
|
||||
conn: &PeekedConn{
|
||||
Peeked: peeked,
|
||||
Conn: conn,
|
||||
host: host,
|
||||
isHTTP: isHTTP,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
// SocksAdapter is a adapter for socks and redir connection
|
||||
type SocksAdapter struct {
|
||||
conn net.Conn
|
||||
addr *C.Addr
|
||||
}
|
||||
|
||||
// Close socks and redir connection
|
||||
func (s *SocksAdapter) Close() {
|
||||
s.conn.Close()
|
||||
}
|
||||
|
||||
// Addr return destination address
|
||||
func (s *SocksAdapter) Addr() *C.Addr {
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Conn return raw net.Conn
|
||||
func (s *SocksAdapter) Conn() net.Conn {
|
||||
return s.conn
|
||||
}
|
||||
|
||||
// NewSocks is SocksAdapter generator
|
||||
func NewSocks(target socks.Addr, conn net.Conn) *SocksAdapter {
|
||||
return &SocksAdapter{
|
||||
conn: conn,
|
||||
addr: parseSocksAddr(target),
|
||||
}
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
func parseSocksAddr(target socks.Addr) *C.Addr {
|
||||
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.Addr{
|
||||
NetWork: C.TCP,
|
||||
AddrType: int(target[0]),
|
||||
Host: host,
|
||||
IP: &ip,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
func parseHTTPAddr(target string) *C.Addr {
|
||||
host, port, _ := net.SplitHostPort(target)
|
||||
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.Addr{
|
||||
NetWork: C.TCP,
|
||||
AddrType: addType,
|
||||
Host: host,
|
||||
IP: resolveIP,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// ParserHTTPHostHeader returns the HTTP Host header from br without
|
||||
// consuming any of its bytes. It returns "" if it can't find one.
|
||||
func ParserHTTPHostHeader(br *bufio.Reader) (method, host string) {
|
||||
// br := bufio.NewReader(bytes.NewReader(data))
|
||||
const maxPeek = 4 << 10
|
||||
peekSize := 0
|
||||
for {
|
||||
peekSize++
|
||||
if peekSize > maxPeek {
|
||||
b, _ := br.Peek(br.Buffered())
|
||||
return method, httpHostHeaderFromBytes(b)
|
||||
}
|
||||
b, err := br.Peek(peekSize)
|
||||
if n := br.Buffered(); n > peekSize {
|
||||
b, _ = br.Peek(n)
|
||||
peekSize = n
|
||||
}
|
||||
if len(b) > 0 {
|
||||
if b[0] < 'A' || b[0] > 'Z' {
|
||||
// Doesn't look like an HTTP verb
|
||||
// (GET, POST, etc).
|
||||
return
|
||||
}
|
||||
if bytes.Index(b, crlfcrlf) != -1 || bytes.Index(b, lflf) != -1 {
|
||||
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Header["Host"]) > 1 {
|
||||
// TODO(bradfitz): what does
|
||||
// ReadRequest do if there are
|
||||
// multiple Host headers?
|
||||
return
|
||||
}
|
||||
return req.Method, req.Host
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return method, httpHostHeaderFromBytes(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
lfHostColon = []byte("\nHost:")
|
||||
lfhostColon = []byte("\nhost:")
|
||||
crlf = []byte("\r\n")
|
||||
lf = []byte("\n")
|
||||
crlfcrlf = []byte("\r\n\r\n")
|
||||
lflf = []byte("\n\n")
|
||||
)
|
||||
|
||||
func httpHostHeaderFromBytes(b []byte) string {
|
||||
if i := bytes.Index(b, lfHostColon); i != -1 {
|
||||
return string(bytes.TrimSpace(untilEOL(b[i+len(lfHostColon):])))
|
||||
}
|
||||
if i := bytes.Index(b, lfhostColon); i != -1 {
|
||||
return string(bytes.TrimSpace(untilEOL(b[i+len(lfhostColon):])))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// untilEOL returns v, truncated before the first '\n' byte, if any.
|
||||
// The returned slice may include a '\r' at the end.
|
||||
func untilEOL(v []byte) []byte {
|
||||
if i := bytes.IndexByte(v, '\n'); i != -1 {
|
||||
return v[:i]
|
||||
}
|
||||
return v
|
||||
}
|
||||
@ -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(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
c, err := net.Dial("tcp", net.JoinHostPort(addr.String(), addr.Port))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.(*net.TCPConn).SetKeepAlive(true)
|
||||
return &DirectAdapter{conn: c}, nil
|
||||
}
|
||||
|
||||
func NewDirect() *Direct {
|
||||
return &Direct{}
|
||||
}
|
||||
@ -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(addr *C.Addr) (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 }
|
||||
@ -1,67 +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
|
||||
}
|
||||
|
||||
func (s *Selector) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *Selector) Type() C.AdapterType {
|
||||
return C.Selector
|
||||
}
|
||||
|
||||
func (s *Selector) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
return s.selected.Generator(addr)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/riobard/go-shadowsocks2/core"
|
||||
"github.com/riobard/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
|
||||
cipher core.Cipher
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) Name() string {
|
||||
return ss.name
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) Type() C.AdapterType {
|
||||
return C.Shadowsocks
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
c, err := net.Dial("tcp", ss.server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", ss.server)
|
||||
}
|
||||
c.(*net.TCPConn).SetKeepAlive(true)
|
||||
c = ss.cipher.StreamConn(c)
|
||||
_, err = c.Write(serializesSocksAddr(addr))
|
||||
return &ShadowsocksAdapter{conn: c}, err
|
||||
}
|
||||
|
||||
func NewShadowSocks(name string, ssURL string) (*ShadowSocks, error) {
|
||||
var key []byte
|
||||
server, cipher, password, _ := parseURL(ssURL)
|
||||
ciph, err := core.PickCipher(cipher, key, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
||||
}
|
||||
return &ShadowSocks{
|
||||
server: server,
|
||||
name: name,
|
||||
cipher: ciph,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseURL(s string) (addr, cipher, password string, err error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
addr = u.Host
|
||||
if u.User != nil {
|
||||
cipher = u.User.Username()
|
||||
password, _ = u.User.Password()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func serializesSocksAddr(addr *C.Addr) []byte {
|
||||
var buf [][]byte
|
||||
aType := uint8(addr.AddrType)
|
||||
p, _ := strconv.Atoi(addr.Port)
|
||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
||||
switch addr.AddrType {
|
||||
case socks.AtypDomainName:
|
||||
len := uint8(len(addr.Host))
|
||||
host := []byte(addr.Host)
|
||||
buf = [][]byte{{aType, len}, host, port}
|
||||
case socks.AtypIPv4:
|
||||
host := addr.IP.To4()
|
||||
buf = [][]byte{{aType}, host, port}
|
||||
case socks.AtypIPv6:
|
||||
host := addr.IP.To16()
|
||||
buf = [][]byte{{aType}, host, port}
|
||||
}
|
||||
return bytes.Join(buf, []byte(""))
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/riobard/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
|
||||
}
|
||||
|
||||
func (ss *Socks5) Name() string {
|
||||
return ss.name
|
||||
}
|
||||
|
||||
func (ss *Socks5) Type() C.AdapterType {
|
||||
return C.Socks5
|
||||
}
|
||||
|
||||
func (ss *Socks5) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
c, err := net.Dial("tcp", ss.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", ss.addr)
|
||||
}
|
||||
c.(*net.TCPConn).SetKeepAlive(true)
|
||||
|
||||
if err := ss.sharkHand(addr, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Socks5Adapter{conn: c}, nil
|
||||
}
|
||||
|
||||
func (ss *Socks5) sharkHand(addr *C.Addr, 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{[]byte{5, 1, 0}, serializesSocksAddr(addr)}, []byte(""))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSocks5(name, addr string) *Socks5 {
|
||||
return &Socks5{
|
||||
addr: addr,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type URLTest struct {
|
||||
name string
|
||||
proxies []C.Proxy
|
||||
rawURL string
|
||||
fast C.Proxy
|
||||
delay time.Duration
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
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(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
|
||||
return u.fast.Generator(addr)
|
||||
}
|
||||
|
||||
func (u *URLTest) Close() {
|
||||
u.done <- struct{}{}
|
||||
}
|
||||
|
||||
func (u *URLTest) loop() {
|
||||
tick := time.NewTicker(u.delay)
|
||||
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.delay)
|
||||
|
||||
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(name string, proxies []C.Proxy, rawURL string, delay time.Duration) (*URLTest, error) {
|
||||
_, err := urlToAddr(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
urlTest := &URLTest{
|
||||
name: name,
|
||||
proxies: proxies[:],
|
||||
rawURL: rawURL,
|
||||
fast: proxies[0],
|
||||
delay: delay,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go urlTest.loop()
|
||||
return urlTest, nil
|
||||
}
|
||||
@ -1,86 +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 := urlToAddr(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}
|
||||
req, err := client.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Body.Close()
|
||||
t = int16(time.Since(start) / time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
||||
func urlToAddr(rawURL string) (addr C.Addr, 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.Addr{
|
||||
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
|
||||
}
|
||||
373
config/config.go
373
config/config.go
@ -1,373 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/remote"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/observable"
|
||||
R "github.com/Dreamacro/clash/rules"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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() (*ini.File, error) {
|
||||
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
return ini.LoadSources(
|
||||
ini.LoadOptions{AllowBooleanKeys: true},
|
||||
C.ConfigPath,
|
||||
)
|
||||
}
|
||||
|
||||
// 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 *ini.File) error {
|
||||
general := cfg.Section("General")
|
||||
|
||||
port := general.Key("port").RangeInt(0, 1, 65535)
|
||||
socksPort := general.Key("socks-port").RangeInt(0, 1, 65535)
|
||||
redirPort := general.Key("redir-port").RangeInt(0, 1, 65535)
|
||||
allowLan := general.Key("allow-lan").MustBool()
|
||||
logLevelString := general.Key("log-level").MustString(C.INFO.String())
|
||||
modeString := general.Key("mode").MustString(Rule.String())
|
||||
|
||||
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 := general.Key("external-controller").String(); restAddr != "" {
|
||||
c.event <- &Event{Type: "external-controller", Payload: restAddr}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (pc.AllowLan != nil || pc.Port != nil) && *pc.Port != 0 {
|
||||
c.general.Port = *pc.Port
|
||||
c.event <- &Event{Type: "http-addr", Payload: genAddr(*pc.Port, c.general.AllowLan)}
|
||||
}
|
||||
|
||||
if (pc.AllowLan != nil || pc.SocksPort != nil) && *pc.SocksPort != 0 {
|
||||
c.general.SocksPort = *pc.SocksPort
|
||||
c.event <- &Event{Type: "socks-addr", Payload: genAddr(*pc.SocksPort, c.general.AllowLan)}
|
||||
}
|
||||
|
||||
if (pc.AllowLan != nil || pc.RedirPort != nil) && *pc.RedirPort != 0 {
|
||||
c.general.RedirPort = *pc.RedirPort
|
||||
c.event <- &Event{Type: "redir-addr", Payload: genAddr(*pc.RedirPort, c.general.AllowLan)}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) parseProxies(cfg *ini.File) error {
|
||||
proxies := make(map[string]C.Proxy)
|
||||
proxiesConfig := cfg.Section("Proxy")
|
||||
groupsConfig := cfg.Section("Proxy Group")
|
||||
|
||||
// parse proxy
|
||||
for _, key := range proxiesConfig.Keys() {
|
||||
proxy := key.Strings(",")
|
||||
if len(proxy) == 0 {
|
||||
continue
|
||||
}
|
||||
switch proxy[0] {
|
||||
// ss, server, port, cipter, password
|
||||
case "ss":
|
||||
if len(proxy) < 5 {
|
||||
continue
|
||||
}
|
||||
ssURL := fmt.Sprintf("ss://%s:%s@%s:%s", proxy[3], proxy[4], proxy[1], proxy[2])
|
||||
ss, err := adapters.NewShadowSocks(key.Name(), ssURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proxies[key.Name()] = ss
|
||||
// socks5, server, port
|
||||
case "socks5":
|
||||
if len(proxy) < 3 {
|
||||
continue
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2])
|
||||
socks5 := adapters.NewSocks5(key.Name(), addr)
|
||||
proxies[key.Name()] = socks5
|
||||
}
|
||||
}
|
||||
|
||||
// parse proxy group
|
||||
for _, key := range groupsConfig.Keys() {
|
||||
rule := strings.Split(key.Value(), ",")
|
||||
rule = trimArr(rule)
|
||||
switch rule[0] {
|
||||
case "url-test":
|
||||
if len(rule) < 4 {
|
||||
return fmt.Errorf("URLTest need more than 4 param")
|
||||
}
|
||||
proxyNames := rule[1 : len(rule)-2]
|
||||
delay, _ := strconv.Atoi(rule[len(rule)-1])
|
||||
url := rule[len(rule)-2]
|
||||
var ps []C.Proxy
|
||||
for _, name := range proxyNames {
|
||||
if p, ok := proxies[name]; ok {
|
||||
ps = append(ps, p)
|
||||
}
|
||||
}
|
||||
|
||||
adapter, err := adapters.NewURLTest(key.Name(), ps, url, time.Duration(delay)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Config error: %s", err.Error())
|
||||
}
|
||||
proxies[key.Name()] = adapter
|
||||
case "select":
|
||||
if len(rule) < 2 {
|
||||
return fmt.Errorf("Selector need more than 2 param")
|
||||
}
|
||||
proxyNames := rule[1:]
|
||||
selectProxy := make(map[string]C.Proxy)
|
||||
for _, name := range proxyNames {
|
||||
proxy, exist := proxies[name]
|
||||
if !exist {
|
||||
return fmt.Errorf("Proxy %s not exist", name)
|
||||
}
|
||||
selectProxy[name] = proxy
|
||||
}
|
||||
selector, err := adapters.NewSelector(key.Name(), selectProxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Selector create error: %s", err.Error())
|
||||
}
|
||||
proxies[key.Name()] = selector
|
||||
}
|
||||
}
|
||||
|
||||
// init proxy
|
||||
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", proxies)
|
||||
proxies["DIRECT"] = adapters.NewDirect()
|
||||
proxies["REJECT"] = adapters.NewReject()
|
||||
|
||||
c.proxies = proxies
|
||||
c.event <- &Event{Type: "proxies", Payload: proxies}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) parseRules(cfg *ini.File) error {
|
||||
rules := []C.Rule{}
|
||||
|
||||
rulesConfig := cfg.Section("Rule")
|
||||
// parse rules
|
||||
for _, key := range rulesConfig.Keys() {
|
||||
rule := strings.Split(key.Name(), ",")
|
||||
if len(rule) < 3 {
|
||||
continue
|
||||
}
|
||||
rule = trimArr(rule)
|
||||
switch rule[0] {
|
||||
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 %s error", c.general.Port)
|
||||
c.general.Port = 0
|
||||
}
|
||||
case "socks-addr":
|
||||
if event.Payload.(bool) == false {
|
||||
log.Errorf("Listening SOCKS proxy at %s error", c.general.SocksPort)
|
||||
c.general.SocksPort = 0
|
||||
}
|
||||
case "redir-addr":
|
||||
if event.Payload.(bool) == false {
|
||||
log.Errorf("Listening Redir proxy at %s 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
|
||||
}
|
||||
@ -1,72 +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 config.ini
|
||||
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
|
||||
log.Info("Can't find config, create a empty file")
|
||||
os.OpenFile(C.ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
// initial mmdb
|
||||
if _, err := os.Stat(C.MMDBPath); os.IsNotExist(err) {
|
||||
log.Info("Can't find MMDB, start download")
|
||||
err := downloadMMDB(C.MMDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't download MMDB: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,20 +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)
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Adapter Type
|
||||
const (
|
||||
Direct AdapterType = iota
|
||||
Reject
|
||||
Selector
|
||||
Shadowsocks
|
||||
Socks5
|
||||
URLTest
|
||||
)
|
||||
|
||||
type ProxyAdapter interface {
|
||||
Conn() net.Conn
|
||||
Close()
|
||||
}
|
||||
|
||||
type ServerAdapter interface {
|
||||
Addr() *Addr
|
||||
Close()
|
||||
}
|
||||
|
||||
type Proxy interface {
|
||||
Name() string
|
||||
Type() AdapterType
|
||||
Generator(addr *Addr) (ProxyAdapter, error)
|
||||
}
|
||||
|
||||
// AdapterType is enum of adapter type
|
||||
type AdapterType int
|
||||
|
||||
func (at AdapterType) String() string {
|
||||
switch at {
|
||||
case Direct:
|
||||
return "Direct"
|
||||
case Reject:
|
||||
return "Reject"
|
||||
case Selector:
|
||||
return "Selector"
|
||||
case Shadowsocks:
|
||||
return "Shadowsocks"
|
||||
case Socks5:
|
||||
return "Socks5"
|
||||
case URLTest:
|
||||
return "URLTest"
|
||||
default:
|
||||
return "Unknow"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
// Addr is used to store connection address
|
||||
type Addr struct {
|
||||
NetWork NetWork
|
||||
Source SourceType
|
||||
AddrType int
|
||||
Host string
|
||||
IP *net.IP
|
||||
Port string
|
||||
}
|
||||
|
||||
func (addr *Addr) String() string {
|
||||
if addr.Host == "" {
|
||||
return addr.IP.String()
|
||||
}
|
||||
return addr.Host
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "clash"
|
||||
)
|
||||
|
||||
var (
|
||||
HomeDir string
|
||||
ConfigPath string
|
||||
MMDBPath string
|
||||
)
|
||||
|
||||
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"`
|
||||
LogLevel *string `json:"log-level,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
dir := os.Getenv("HOME")
|
||||
if dir == "" {
|
||||
log.Fatalf("Can't get current user: %s", err.Error())
|
||||
}
|
||||
HomeDir = dir
|
||||
} else {
|
||||
HomeDir = currentUser.HomeDir
|
||||
}
|
||||
|
||||
dirPath := path.Join(HomeDir, ".config", Name)
|
||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dirPath, 0777); err != nil {
|
||||
log.Fatalf("Can't create config directory %s: %s", dirPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
ConfigPath = path.Join(dirPath, "config.ini")
|
||||
MMDBPath = path.Join(dirPath, "Country.mmdb")
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package constant
|
||||
|
||||
// ProxySignal is used to handle graceful shutdown of proxy
|
||||
type ProxySignal struct {
|
||||
Done chan<- struct{}
|
||||
Closed <-chan struct{}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package constant
|
||||
|
||||
// Rule Type
|
||||
const (
|
||||
DomainSuffix RuleType = iota
|
||||
DomainKeyword
|
||||
GEOIP
|
||||
IPCIDR
|
||||
FINAL
|
||||
)
|
||||
|
||||
type RuleType int
|
||||
|
||||
func (rt RuleType) String() string {
|
||||
switch rt {
|
||||
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(addr *Addr) bool
|
||||
Adapter() string
|
||||
Payload() string
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
BIN
docs/logo.png
BIN
docs/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
44
examples/basic.py
Normal file
44
examples/basic.py
Normal 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())
|
||||
25
examples/data_persistence.py
Normal file
25
examples/data_persistence.py
Normal 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
19
examples/merge_data.py
Normal 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())
|
||||
@ -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}
|
||||
}
|
||||
@ -1,93 +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"`
|
||||
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,
|
||||
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,
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
193
hub/proxies.go
193
hub/proxies.go
@ -1,193 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
A "github.com/Dreamacro/clash/adapters/remote"
|
||||
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"`
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
53
hub/rules.go
53
hub/rules.go
@ -1,53 +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"`
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
142
hub/server.go
142
hub/server.go
@ -1,142 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"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"
|
||||
)
|
||||
|
||||
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{}{}
|
||||
for {
|
||||
elm := <-ch
|
||||
event := elm.(*config.Event)
|
||||
if event.Type == "external-controller" {
|
||||
addr = event.Payload.(string)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Content-Type"},
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
r.Use(cors.Handler)
|
||||
|
||||
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 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 GetLogs struct {
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Type string `json:"type"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
func getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetLogs{}
|
||||
render.DecodeJSON(r.Body, req)
|
||||
if req.Level == "" {
|
||||
req.Level = "info"
|
||||
}
|
||||
|
||||
level, ok := C.LogLevelMapping[req.Level]
|
||||
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
|
||||
}
|
||||
30
main.go
30
main.go
@ -1,30 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/Dreamacro/clash/config"
|
||||
"github.com/Dreamacro/clash/hub"
|
||||
"github.com/Dreamacro/clash/proxy"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tunnel.Instance().Run()
|
||||
proxy.Instance().Run()
|
||||
hub.Run()
|
||||
|
||||
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
4
mihomo/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from . import tools
|
||||
from .client import *
|
||||
from .errors import *
|
||||
from .models import *
|
||||
156
mihomo/client.py
Normal file
156
mihomo/client.py
Normal 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
42
mihomo/errors.py
Normal 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"
|
||||
6
mihomo/models/__init__.py
Normal file
6
mihomo/models/__init__.py
Normal 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
19
mihomo/models/base.py
Normal 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"""
|
||||
92
mihomo/models/character.py
Normal file
92
mihomo/models/character.py
Normal 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
112
mihomo/models/combat.py
Normal 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
118
mihomo/models/equipment.py
Normal 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
97
mihomo/models/player.py
Normal 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
97
mihomo/models/stat.py
Normal 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"""
|
||||
4
mihomo/models/v1/__init__.py
Normal file
4
mihomo/models/v1/__init__.py
Normal 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
22
mihomo/models/v1/base.py
Normal 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"""
|
||||
150
mihomo/models/v1/character.py
Normal file
150
mihomo/models/v1/character.py
Normal 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"
|
||||
71
mihomo/models/v1/equipment.py
Normal file
71
mihomo/models/v1/equipment.py
Normal 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
|
||||
65
mihomo/models/v1/player.py
Normal file
65
mihomo/models/v1/player.py
Normal 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
107
mihomo/tools.py
Normal 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
|
||||
@ -1,3 +0,0 @@
|
||||
package observable
|
||||
|
||||
type Iterable <-chan interface{}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
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)
|
||||
method, hostName := adapters.ParserHTTPHostHeader(br)
|
||||
if hostName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(hostName, ":") {
|
||||
hostName += ":80"
|
||||
}
|
||||
|
||||
var peeked []byte
|
||||
if method == http.MethodConnect {
|
||||
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if n := br.Buffered(); n > 0 {
|
||||
peeked, _ = br.Peek(br.Buffered())
|
||||
}
|
||||
|
||||
tun.Add(adapters.NewHTTP(hostName, peeked, method != http.MethodConnect, conn))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
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.NewSocks(target, conn))
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/riobard/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
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/riobard/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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/riobard/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
func parserPacket(conn net.Conn) (socks.Addr, error) {
|
||||
return nil, errors.New("Windows not support yet")
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/riobard/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.NewSocks(target, conn))
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
package socks
|
||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal 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.*",
|
||||
]
|
||||
@ -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(addr *C.Addr) bool {
|
||||
if addr.AddrType != C.AtypDomainName {
|
||||
return false
|
||||
}
|
||||
domain := addr.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,
|
||||
}
|
||||
}
|
||||
@ -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(addr *C.Addr) bool {
|
||||
if addr.AddrType != C.AtypDomainName {
|
||||
return false
|
||||
}
|
||||
domain := addr.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,
|
||||
}
|
||||
}
|
||||
@ -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(addr *C.Addr) 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,
|
||||
}
|
||||
}
|
||||
@ -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(addr *C.Addr) bool {
|
||||
if addr.IP == nil {
|
||||
return false
|
||||
}
|
||||
record, _ := mmdb.Country(*addr.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.MMDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't load mmdb: %s", err.Error())
|
||||
}
|
||||
})
|
||||
return &GEOIP{
|
||||
country: country,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
@ -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(addr *C.Addr) bool {
|
||||
if addr.IP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return i.ipnet.Contains(*addr.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,
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/local"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
|
||||
conn := newTrafficTrack(proxy.Conn(), t.traffic)
|
||||
|
||||
// Before we unwrap src and/or dst, copy any buffered data.
|
||||
if wc, ok := request.Conn().(*adapters.PeekedConn); ok && len(wc.Peeked) > 0 {
|
||||
if _, err := conn.Write(wc.Peeked); err != nil {
|
||||
return
|
||||
}
|
||||
wc.Peeked = nil
|
||||
}
|
||||
|
||||
go io.Copy(request.Conn(), conn)
|
||||
io.Copy(conn, request.Conn())
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleSOCKS(request *adapters.SocksAdapter, proxy C.ProxyAdapter) {
|
||||
conn := newTrafficTrack(proxy.Conn(), t.traffic)
|
||||
go io.Copy(request.Conn(), conn)
|
||||
io.Copy(conn, request.Conn())
|
||||
}
|
||||
@ -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...),
|
||||
}
|
||||
}
|
||||
161
tunnel/tunnel.go
161
tunnel/tunnel.go
@ -1,161 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
LocalAdapter "github.com/Dreamacro/clash/adapters/local"
|
||||
cfg "github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/observable"
|
||||
|
||||
"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()
|
||||
addr := localConn.Addr()
|
||||
|
||||
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(addr)
|
||||
}
|
||||
remoConn, err := proxy.Generator(addr)
|
||||
if err != nil {
|
||||
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
defer remoConn.Close()
|
||||
|
||||
switch adapter := localConn.(type) {
|
||||
case *LocalAdapter.HTTPAdapter:
|
||||
t.handleHTTP(adapter, remoConn)
|
||||
case *LocalAdapter.SocksAdapter:
|
||||
t.handleSOCKS(adapter, remoConn)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) match(addr *C.Addr) C.Proxy {
|
||||
t.configLock.RLock()
|
||||
defer t.configLock.RUnlock()
|
||||
|
||||
for _, rule := range t.rules {
|
||||
if rule.IsMatch(addr) {
|
||||
a, ok := t.proxies[rule.Adapter()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
t.logCh <- newLog(C.INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
|
||||
return a
|
||||
}
|
||||
}
|
||||
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", addr.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
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user