Blame view

publish/skynet/lualib/snax/loginserver.lua 5.19 KB
4d6f285d   zhouhaihai   增加发布功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
  local skynet = require "skynet"
  require "skynet.manager"
  local socket = require "skynet.socket"
  local crypt = require "skynet.crypt"
  local table = table
  local string = string
  local assert = assert
  
  --[[
  
  Protocol:
  
  	line (\n) based text protocol
  
  	1. Server->Client : base64(8bytes random challenge)
  	2. Client->Server : base64(8bytes handshake client key)
  	3. Server: Gen a 8bytes handshake server key
  	4. Server->Client : base64(DH-Exchange(server key))
  	5. Server/Client secret := DH-Secret(client key/server key)
  	6. Client->Server : base64(HMAC(challenge, secret))
  	7. Client->Server : DES(secret, base64(token))
  	8. Server : call auth_handler(token) -> server, uid (A user defined method)
  	9. Server : call login_handler(server, uid, secret) ->subid (A user defined method)
  	10. Server->Client : 200 base64(subid)
  
  Error Code:
  	401 Unauthorized . unauthorized by auth_handler
  	403 Forbidden . login_handler failed
  	406 Not Acceptable . already in login (disallow multi login)
  
  Success:
  	200 base64(subid)
  ]]
  
  local socket_error = {}
  local function assert_socket(service, v, fd)
  	if v then
  		return v
  	else
  		skynet.error(string.format("%s failed: socket (fd = %d) closed", service, fd))
  		error(socket_error)
  	end
  end
  
  local function write(service, fd, text)
  	assert_socket(service, socket.write(fd, text), fd)
  end
  
  local function launch_slave(auth_handler)
  	local function auth(fd, addr)
  		-- set socket buffer limit (8K)
  		-- If the attacker send large package, close the socket
  		socket.limit(fd, 8192)
  
  		local challenge = crypt.randomkey()
  		write("auth", fd, crypt.base64encode(challenge).."\n")
  
  		local handshake = assert_socket("auth", socket.readline(fd), fd)
  		local clientkey = crypt.base64decode(handshake)
  		if #clientkey ~= 8 then
  			error "Invalid client key"
  		end
  		local serverkey = crypt.randomkey()
  		write("auth", fd, crypt.base64encode(crypt.dhexchange(serverkey)).."\n")
  
  		local secret = crypt.dhsecret(clientkey, serverkey)
  
  		local response = assert_socket("auth", socket.readline(fd), fd)
  		local hmac = crypt.hmac64(challenge, secret)
  
  		if hmac ~= crypt.base64decode(response) then
  			error "challenge failed"
  		end
  
  		local etoken = assert_socket("auth", socket.readline(fd),fd)
  
  		local token = crypt.desdecode(secret, crypt.base64decode(etoken))
  
  		local ok, server, uid =  pcall(auth_handler,token)
  
  		return ok, server, uid, secret
  	end
  
  	local function ret_pack(ok, err, ...)
  		if ok then
  			return skynet.pack(err, ...)
  		else
  			if err == socket_error then
  				return skynet.pack(nil, "socket error")
  			else
  				return skynet.pack(false, err)
  			end
  		end
  	end
  
  	local function auth_fd(fd, addr)
  		skynet.error(string.format("connect from %s (fd = %d)", addr, fd))
  		socket.start(fd)	-- may raise error here
  		local msg, len = ret_pack(pcall(auth, fd, addr))
  		socket.abandon(fd)	-- never raise error here
  		return msg, len
  	end
  
  	skynet.dispatch("lua", function(_,_,...)
  		local ok, msg, len = pcall(auth_fd, ...)
  		if ok then
  			skynet.ret(msg,len)
  		else
  			skynet.ret(skynet.pack(false, msg))
  		end
  	end)
  end
  
  local user_login = {}
  
  local function accept(conf, s, fd, addr)
  	-- call slave auth
  	local ok, server, uid, secret = skynet.call(s, "lua",  fd, addr)
  	-- slave will accept(start) fd, so we can write to fd later
  
  	if not ok then
  		if ok ~= nil then
  			write("response 401", fd, "401 Unauthorized\n")
  		end
  		error(server)
  	end
  
  	if not conf.multilogin then
  		if user_login[uid] then
  			write("response 406", fd, "406 Not Acceptable\n")
  			error(string.format("User %s is already login", uid))
  		end
  
  		user_login[uid] = true
  	end
  
  	local ok, err = pcall(conf.login_handler, server, uid, secret)
  	-- unlock login
  	user_login[uid] = nil
  
  	if ok then
  		err = err or ""
  		write("response 200",fd,  "200 "..crypt.base64encode(err).."\n")
  	else
  		write("response 403",fd,  "403 Forbidden\n")
  		error(err)
  	end
  end
  
  local function launch_master(conf)
  	local instance = conf.instance or 8
  	assert(instance > 0)
  	local host = conf.host or "0.0.0.0"
  	local port = assert(tonumber(conf.port))
  	local slave = {}
  	local balance = 1
  
  	skynet.dispatch("lua", function(_,source,command, ...)
  		skynet.ret(skynet.pack(conf.command_handler(command, ...)))
  	end)
  
  	for i=1,instance do
  		table.insert(slave, skynet.newservice(SERVICE_NAME))
  	end
  
  	skynet.error(string.format("login server listen at : %s %d", host, port))
  	local id = socket.listen(host, port)
  	socket.start(id , function(fd, addr)
  		local s = slave[balance]
  		balance = balance + 1
  		if balance > #slave then
  			balance = 1
  		end
  		local ok, err = pcall(accept, conf, s, fd, addr)
  		if not ok then
  			if err ~= socket_error then
  				skynet.error(string.format("invalid client (fd = %d) error = %s", fd, err))
  			end
  		end
  		socket.close_fd(fd)	-- We haven't call socket.start, so use socket.close_fd rather than socket.close.
  	end)
  end
  
  local function login(conf)
  	local name = "." .. (conf.name or "login")
  	skynet.start(function()
  		local loginmaster = skynet.localname(name)
  		if loginmaster then
  			local auth_handler = assert(conf.auth_handler)
  			launch_master = nil
  			conf = nil
  			launch_slave(auth_handler)
  		else
  			launch_slave = nil
  			conf.auth_handler = nil
  			assert(conf.login_handler)
  			assert(conf.command_handler)
  			skynet.register(name)
  			launch_master(conf)
  		end
  	end)
  end
  
  return login