Browse Source

Initial commit

kotoyuuko 1 year ago
commit
5c286a8f98
15 changed files with 447 additions and 0 deletions
  1. 4 0
      .gitignore
  2. 14 0
      .gitlab-ci.yml
  3. 40 0
      .goreleaser.yaml
  4. 25 0
      app/cmd/cmd.go
  5. 44 0
      app/cmd/run.go
  6. 15 0
      app/func/hosts/hosts.go
  7. 36 0
      app/func/resolver/resolver.go
  8. 8 0
      app/func/writer/writer.go
  9. 28 0
      app/model/doh.go
  10. 7 0
      config.yaml
  11. 29 0
      go.mod
  12. 36 0
      main.go
  13. 85 0
      pkg/config/config.go
  14. 40 0
      pkg/console/console.go
  15. 36 0
      pkg/helpers/helpers.go

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.idea/
+dist/
+.DS_Store
+go.sum

+ 14 - 0
.gitlab-ci.yml

@@ -0,0 +1,14 @@
+stages:
+  - release
+
+release:
+  stage: release
+  image:
+    name: goreleaser/goreleaser
+    entrypoint: ['']
+  only:
+    - tags
+  variables:
+    GIT_DEPTH: 0
+  script:
+    - goreleaser release --clean

+ 40 - 0
.goreleaser.yaml

@@ -0,0 +1,40 @@
+before:
+  hooks:
+    - go mod tidy
+    - go generate ./...
+builds:
+  - env:
+      - CGO_ENABLED=0
+    goos:
+      - linux
+      - windows
+      - darwin
+
+archives:
+  - format: tar.gz
+    name_template: >-
+      {{ .ProjectName }}_
+      {{- title .Os }}_
+      {{- if eq .Arch "amd64" }}x86_64
+      {{- else if eq .Arch "386" }}i386
+      {{- else }}{{ .Arch }}{{ end }}
+      {{- if .Arm }}v{{ .Arm }}{{ end }}
+    format_overrides:
+    - goos: windows
+      format: zip
+checksum:
+  name_template: 'checksums.txt'
+snapshot:
+  name_template: "{{ incpatch .Version }}-next"
+changelog:
+  sort: asc
+  filters:
+    exclude:
+      - '^docs:'
+      - '^test:'
+gitlab_urls:
+  api: https://moe.codes/api/v4/
+  download: https://moe.codes
+  skip_tls_verify: false
+  use_package_registry: false
+  use_job_token: true

+ 25 - 0
app/cmd/cmd.go

@@ -0,0 +1,25 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+	"os"
+	"rinne.dev/doh-resolver/pkg/helpers"
+)
+
+// Config 存储配置文件
+var Config string
+
+// RegisterGlobalFlags 注册全局选项(flag)
+func RegisterGlobalFlags(rootCmd *cobra.Command) {
+	rootCmd.PersistentFlags().StringVarP(&Config, "config", "c", "config.yaml", "load config file")
+}
+
+// RegisterDefaultCmd 注册默认命令
+func RegisterDefaultCmd(rootCmd *cobra.Command, subCmd *cobra.Command) {
+	cmd, _, err := rootCmd.Find(os.Args[1:])
+	firstArg := helpers.FirstElement(os.Args[1:])
+	if err == nil && cmd.Use == rootCmd.Use && firstArg != "-h" && firstArg != "--help" {
+		args := append([]string{subCmd.Use}, os.Args[1:]...)
+		rootCmd.SetArgs(args)
+	}
+}

+ 44 - 0
app/cmd/run.go

@@ -0,0 +1,44 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/spf13/cobra"
+	"rinne.dev/doh-resolver/app/func/hosts"
+	"rinne.dev/doh-resolver/app/func/resolver"
+	"rinne.dev/doh-resolver/app/func/writer"
+	"rinne.dev/doh-resolver/pkg/config"
+)
+
+// Run 运行 DoH 查询
+var Run = &cobra.Command{
+	Use:   "run",
+	Short: "Run DoH query and save to file",
+	Run:   run,
+	Args:  cobra.NoArgs,
+}
+
+func run(cmd *cobra.Command, args []string) {
+	// 获取需要查询的域名
+	domains := config.GetStringArray("domains")
+
+	// DoH 查询
+	resData := make(map[string]string)
+	for _, domain := range domains {
+		ans, err := resolver.Resolve(domain)
+		if err != nil {
+			fmt.Println(err)
+			return
+		}
+		resData[domain] = ans
+	}
+
+	// 生成 hosts 文件
+	hostsFile := hosts.GenerateHostsFile(resData)
+
+	// 写入文件
+	err := writer.WriteFile(config.GetString("output"), hostsFile)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+}

+ 15 - 0
app/func/hosts/hosts.go

@@ -0,0 +1,15 @@
+package hosts
+
+import (
+	"fmt"
+	"strings"
+)
+
+// GenerateHostsFile 生成 hosts 文件
+func GenerateHostsFile(data map[string]string) string {
+	res := make([]string, 0)
+	for host, ip := range data {
+		res = append(res, fmt.Sprintf("%s %s", ip, host))
+	}
+	return strings.Join(res, "\n")
+}

+ 36 - 0
app/func/resolver/resolver.go

@@ -0,0 +1,36 @@
+package resolver
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"rinne.dev/doh-resolver/app/model"
+	"rinne.dev/doh-resolver/pkg/config"
+)
+
+// Resolve 查找域名对应的 IP
+func Resolve(domain string) (string, error) {
+	// 请求 DOH 服务器
+	resp, err := http.Get(config.GetString("resolver") + "?name=" + domain)
+	if err != nil {
+		return "", err
+	}
+	resBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+
+	// 解析响应
+	var response model.DOHResponse
+	err = json.Unmarshal(resBody, &response)
+	if err != nil {
+		return "", err
+	}
+
+	// 结果取到最后一个
+	ans := ""
+	for _, answer := range response.Answer {
+		ans = answer.Data
+	}
+	return ans, nil
+}

+ 8 - 0
app/func/writer/writer.go

@@ -0,0 +1,8 @@
+package writer
+
+import "os"
+
+// WriteFile 写入文件
+func WriteFile(filename, content string) error {
+	return os.WriteFile(filename, []byte(content), 0644)
+}

+ 28 - 0
app/model/doh.go

@@ -0,0 +1,28 @@
+package model
+
+// DOHResponse Google DOH 响应
+type DOHResponse struct {
+	Status   int           `json:"Status"`
+	Comment  string        `json:"Comment"`
+	TC       bool          `json:"TC"`
+	RD       bool          `json:"RD"`
+	RA       bool          `json:"RA"`
+	AD       bool          `json:"AD"`
+	CD       bool          `json:"CD"`
+	Question []DOHQuestion `json:"Question"`
+	Answer   []DOHAnswer   `json:"Answer"`
+}
+
+// DOHQuestion Google DOH 问题
+type DOHQuestion struct {
+	Name string `json:"name"`
+	Type int    `json:"type"`
+}
+
+// DOHAnswer Google DOH 答复
+type DOHAnswer struct {
+	Name string `json:"name"`
+	Type int    `json:"type"`
+	TTL  int    `json:"TTL"`
+	Data string `json:"data"`
+}

+ 7 - 0
config.yaml

@@ -0,0 +1,7 @@
+# resolver: "https://dns.google/resolve"
+resolver: "https://doh.pub/dns-query"
+output: "/Volumes/Code/wwwroot/servers.hosts"
+domains:
+  - "r.saki.host"
+  - "term.saki.host"
+  - "qb.ioo.im"

+ 29 - 0
go.mod

@@ -0,0 +1,29 @@
+module rinne.dev/doh-resolver
+
+go 1.19
+
+require (
+	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
+	github.com/spf13/cast v1.5.0
+	github.com/spf13/cobra v1.6.1
+	github.com/spf13/viper v1.15.0
+)
+
+require (
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.0.1 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/spf13/afero v1.9.3 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/subosito/gotenv v1.4.2 // indirect
+	golang.org/x/sys v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 36 - 0
main.go

@@ -0,0 +1,36 @@
+package main
+
+import (
+	"fmt"
+	"github.com/spf13/cobra"
+	"os"
+	"rinne.dev/doh-resolver/app/cmd"
+	"rinne.dev/doh-resolver/pkg/config"
+	"rinne.dev/doh-resolver/pkg/console"
+)
+
+func main() {
+	var rootCmd = &cobra.Command{
+		Use:  "doh_resolver",
+		Long: `Default will run "run" command, you can use "-h" flag to see all subcommands`,
+		PersistentPreRun: func(command *cobra.Command, args []string) {
+			config.InitConfig(cmd.Config)
+		},
+	}
+
+	// 注册命令
+	rootCmd.AddCommand(
+		cmd.Run,
+	)
+
+	// 配置默认命令
+	cmd.RegisterDefaultCmd(rootCmd, cmd.Run)
+
+	// 注册全局参数
+	cmd.RegisterGlobalFlags(rootCmd)
+
+	// 执行主命令
+	if err := rootCmd.Execute(); err != nil {
+		console.Exit(fmt.Sprintf("Failed to run app with %v: %s", os.Args, err.Error()))
+	}
+}

+ 85 - 0
pkg/config/config.go

@@ -0,0 +1,85 @@
+package config
+
+import (
+	"github.com/spf13/cast"
+	viperLib "github.com/spf13/viper"
+	"path"
+	"rinne.dev/doh-resolver/pkg/helpers"
+)
+
+// viper 库实例
+var viper *viperLib.Viper
+
+func init() {
+	viper = viperLib.New()
+	viper.SetConfigType("yaml")
+}
+
+// InitConfig 初始化配置信息
+func InitConfig(filePath string) {
+	// 加载配置文件
+	viper.SetConfigName(path.Base(filePath))
+	viper.AddConfigPath(path.Dir(filePath))
+	if err := viper.ReadInConfig(); err != nil {
+		panic(err)
+	}
+
+	// 监控配置文件,变更时重新加载
+	viper.WatchConfig()
+}
+
+// Get 获取配置项
+func Get(path string, defaultValue ...interface{}) string {
+	return GetString(path, defaultValue...)
+}
+
+func internalGet(path string, defaultValue ...interface{}) interface{} {
+	// config 或者环境变量不存在的情况
+	if !viper.IsSet(path) || helpers.Empty(viper.Get(path)) {
+		if len(defaultValue) > 0 {
+			return defaultValue[0]
+		}
+		return nil
+	}
+	return viper.Get(path)
+}
+
+// GetString 获取 String 类型的配置信息
+func GetString(path string, defaultValue ...interface{}) string {
+	return cast.ToString(internalGet(path, defaultValue...))
+}
+
+// GetInt 获取 Int 类型的配置信息
+func GetInt(path string, defaultValue ...interface{}) int {
+	return cast.ToInt(internalGet(path, defaultValue...))
+}
+
+// GetFloat64 获取 float64 类型的配置信息
+func GetFloat64(path string, defaultValue ...interface{}) float64 {
+	return cast.ToFloat64(internalGet(path, defaultValue...))
+}
+
+// GetInt64 获取 Int64 类型的配置信息
+func GetInt64(path string, defaultValue ...interface{}) int64 {
+	return cast.ToInt64(internalGet(path, defaultValue...))
+}
+
+// GetUint 获取 Uint 类型的配置信息
+func GetUint(path string, defaultValue ...interface{}) uint {
+	return cast.ToUint(internalGet(path, defaultValue...))
+}
+
+// GetBool 获取 Bool 类型的配置信息
+func GetBool(path string, defaultValue ...interface{}) bool {
+	return cast.ToBool(internalGet(path, defaultValue...))
+}
+
+// GetStringMapString 获取结构数据
+func GetStringMapString(path string) map[string]string {
+	return viper.GetStringMapString(path)
+}
+
+// GetStringArray 获取字符串数组
+func GetStringArray(path string) []string {
+	return cast.ToStringSlice(internalGet(path))
+}

+ 40 - 0
pkg/console/console.go

@@ -0,0 +1,40 @@
+package console
+
+import (
+	"fmt"
+	"github.com/mgutz/ansi"
+	"os"
+)
+
+// Success 打印一条成功消息,绿色输出
+func Success(msg string) {
+	colorOut(msg, "green")
+}
+
+// Error 打印一条报错消息,红色输出
+func Error(msg string) {
+	colorOut(msg, "red")
+}
+
+// Warning 打印一条提示消息,黄色输出
+func Warning(msg string) {
+	colorOut(msg, "yellow")
+}
+
+// Exit 打印一条报错消息,并退出 os.Exit(1)
+func Exit(msg string) {
+	Error(msg)
+	os.Exit(1)
+}
+
+// ExitIf 语法糖,自带 err != nil 判断
+func ExitIf(err error) {
+	if err != nil {
+		Exit(err.Error())
+	}
+}
+
+// colorOut 内部使用,设置高亮颜色
+func colorOut(message, color string) {
+	_, _ = fmt.Fprintln(os.Stdout, ansi.Color(message, color))
+}

+ 36 - 0
pkg/helpers/helpers.go

@@ -0,0 +1,36 @@
+package helpers
+
+import "reflect"
+
+// Empty 判断变量是否为空
+func Empty(val interface{}) bool {
+	if val == nil {
+		return true
+	}
+	v := reflect.ValueOf(val)
+	switch v.Kind() {
+	case reflect.String, reflect.Array:
+		return v.Len() == 0
+	case reflect.Map, reflect.Slice:
+		return v.Len() == 0 || v.IsNil()
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	}
+	return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
+}
+
+// FirstElement 安全地获取 args[0],避免 panic: runtime error: index out of range
+func FirstElement(args []string) string {
+	if len(args) > 0 {
+		return args[0]
+	}
+	return ""
+}