Write a Small Service for Learning Golang
某天接触到某个用Golang实现的程序,不仅体积小,还支持几乎所有种类的CPU,于是下决心学习一下Go这门语言。虽然刚出来的时候就想学,但那时据说有很多坑(比如语法可能会变),就放弃了。现在连被成为垃圾的包管理也有了升级,觉得是时候去学。恰好要把一个二级域名绑定IP的小服务迁移到VPS上,于是干脆用Go重新实现(原来是用Python3)这个服务。
首先要阅读相关教程。初学者教程当然是官方入门教程:
https://tour.go-zh.org/
官方入门教程太简单(毕竟Go本身语法就是简单),还需要阅读其它相关知识:
1)Go搭建一个Web服务器
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/03.2.md
2)golang读取json配置文件
[https://blog.csdn.net/benben_2015/article/details/79134734]
3)文件读写
https://wiki.jikexueyuan.com/project/the-way-to-go/12.2.html
4)go-extend,获取请求的IP的代码
https://github.com/thinkeridea/go-extend/blob/master/exnet/ip.go
5)golang 发送GET和POST示例
https://segmentfault.com/a/1190000013262746
6)GoDNS中dnspod客户端的代码
https://github.com/TimothyYe/godns/blob/master/handler/dnspod/dnspod_handler.go
7)Go语言(golang)的错误(error)处理的推荐方案
https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html
这个小服务,就是个web服务。客户端发起带有key和token的请求,此服务会验证有效的授权,然后把对应的二级域名与客户端IP绑定。配置信息以json格式保存在文本文件。客户端IP会记录在对应log文件,以方便每次比较客户端IP是否变化了。每次更新二级域名与IP的绑定,则会记录log。相关文件及代码如下:
配置文件,config.json:
{
"ServIpPort": ":12345"
,"DnsKey": "dnspod的key"
,"DnsToken": "dnspod的token"
,"DomainId": "dnspod的域名id"
,"SubDomainId": {"abc":"dnspod的二级域名id", "efg":"dnspod的二级域名id"}
,"Users": [
{
"Key":"client1"
,"Token":"aaa123456"
,"SubDomains": ["abc"]
}
,{
"Key":"client2"
,"Token":"xxx789012"
,"SubDomains": ["efg"]
}
]
}
小服务的代码,ddnsServ.go:
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/url"
"strings"
"os"
"path/filepath"
"io/ioutil"
"time"
"encoding/json"
)
type User struct {
Key string
Token string
SubDomains []string
}
type Config struct {
ServIpPort string
DnsKey string
DnsToken string
DomainId string
SubDomainId map[string]string /*key:sub domain name, value:sub domain id*/
Users []User
}
var (
curPath string
ipLogPath string
historyPath string
config Config
)
func init() {
curPath, _ = filepath.Abs(filepath.Dir(os.Args[0]))
ipLogPath = curPath + "/ip"
historyPath = curPath + "/log"
for _, path := range []string{ipLogPath, historyPath} {
if err := initPath(path); err != nil {
fmt.Printf("Init failed. Error info:%s\n", err)
os.Exit(-1)
return
}
}
if err := initConfig(curPath + "/config.json"); err != nil {
fmt.Printf("Init failed. Error info:%s\n", err)
os.Exit(-1)
return
}
}
func initPath(path string) error {
s, err := os.Stat(path)
if err == nil && !s.IsDir() {
return fmt.Errorf("The path is existed, but it is not a directory! Path is:%s", path)
}
if err != nil && os.IsNotExist(err) {
e := os.Mkdir(path, os.ModePerm)
return e
}
return nil
}
func initConfig(configPath string) error {
configData, err := ioutil.ReadFile(configPath)
if err != nil {
return fmt.Errorf("Failed to read config file: %s! Error info: \n%s", configPath, err)
}
err = json.Unmarshal(configData, &config)
if err != nil {
return fmt.Errorf("Failed to load config data! Error info: \n%s", err)
}
return nil
}
// get client IP address
func GetClientIP(r *http.Request) string {
xForwardedFor := r.Header.Get("X-Forwarded-For")
ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
if ip != "" {
return ip
}
ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
if ip != "" {
return ip
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
return ip
}
return ""
}
func GetLogFilePath(logPath string, subDomain string) string {
return fmt.Sprintf("%s/%s.log", logPath, subDomain)
}
// get the file path which saved ip address of subDomain
func GetIpLog(subDomain string) string {
path := GetLogFilePath(ipLogPath, subDomain)
buf, err := ioutil.ReadFile(path)
if err != nil {
return ""
}
return string(buf)
}
func SaveIpLog(subDomain string, ip string) {
path := GetLogFilePath(ipLogPath, subDomain)
ioutil.WriteFile(path, []byte(ip), 0644)
/*
err := ioutil.WriteFile(path, []byte(ip), 0644)
if err != nil {
panic(err.Error())
}
*/
}
func SaveHistoryLog(subDomain string, ip string) error {
path := GetLogFilePath(historyPath, subDomain)
logFile, err := os.OpenFile(path, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("Failed to open history log file: %s! Error info: \n%s", path, err)
}
defer logFile.Close()
nowStr := time.Now().Format("2006-01-02 15:04:05")
log := fmt.Sprintf("%s, ip:%s\n", nowStr, ip)
logByte := []byte(log)
n, err := logFile.Write(logByte)
if err == nil && n < len(logByte) {
return fmt.Errorf("Failed to save history log file: %s! Error info: \nwrite file failed", path)
}
return nil
}
func UpdateDns(subDomain string, ip string) error {
values := url.Values{}
values.Add("login_token", config.DnsKey + "," + config.DnsToken)
values.Add("format", "json")
values.Add("lang", "en")
values.Add("error_on_empty", "no")
values.Add("domain_id", config.DomainId)
values.Add("record_id", config.SubDomainId[subDomain])
values.Add("sub_domain", subDomain)
values.Add("record_type", "A")
values.Add("record_line", "默认")
values.Add("value", ip)
client := &http.Client{}
req, err := http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(values.Encode()))
if err != nil {
// handle error
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "text/json")
resp, err := client.Do(req)
defer resp.Body.Close()
_, err2 := ioutil.ReadAll(resp.Body)
// fmt.Println(string(s))
if err2 != nil {
return err2
}
return nil
}
func handler(w http.ResponseWriter, r *http.Request) {
// Verify authorization
var subDomains []string
key := r.PostFormValue("key") // get key of POST
token := r.PostFormValue("token") // get token of POST
for _, user := range config.Users {
if user.Key == key && user.Token == token {
subDomains = user.SubDomains
break
}
}
if subDomains == nil || len(subDomains) <= 0 {
w.WriteHeader(http.StatusNotFound)
return
}
// get IP
ip := GetClientIP(r)
fmt.Fprintf(w, "%s", ip)
for _, subDomain := range subDomains {
// get last IP of subDomain
ipLog := GetIpLog(subDomain)
if ip == ipLog {
// IP is not changed
continue
}
// update DNS, bind IP to subDomain
err := UpdateDns(subDomain, ip)
if err == nil {
// update success, save new IP
SaveIpLog(subDomain, ip)
SaveHistoryLog(subDomain, ip)
}
}
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(config.ServIpPort, nil))
}
关于部署,就用nginx弄个反向代理,指向这个小服务的端口。由于VPS上装了Debian9,可以配置systemd来设置系统服务。
客户端只需发起post请求,把key和token发过来就可以了。以下用curl实现:
#!/bin/sh
#curl命令的参数解析:
# -X 请求的方法。这里用了POST,在HTTPS传输中,数据被加密
# --connect-timeout 连接超时时间,单位:秒
# -m/--max-time 数据传输的最大允许时间,单位:秒
# https://rpi.f...... 请求的URL
# -H/--header 请求头。要设置多个请求头,则设置多个-H参数
# -d/--data 请求数据。
curl -k -X POST --connect-timeout 5 -m 10 https://youdomain.xxx:12345/api/update_dns -H 'cache-control: no-cache' -H 'content-type: application/x-www-form-urlencoded' -d 'key=client1&token=aaa123456'
目前这个小服务工作良好。除了体积有点大(约7MB),其它都挺满意的。