mirror of
https://github.com/cloudwu/skynet.git
synced 2026-07-25 12:43:09 +00:00
add snax.gateserver snax.loginserver snax.msgserver
This commit is contained in:
130
lualib/snax/gateserver.lua
Normal file
130
lualib/snax/gateserver.lua
Normal file
@@ -0,0 +1,130 @@
|
||||
local skynet = require "skynet"
|
||||
local netpack = require "netpack"
|
||||
local socketdriver = require "socketdriver"
|
||||
|
||||
local gateserver = {}
|
||||
|
||||
local socket -- listen socket
|
||||
local queue -- message queue
|
||||
local maxclient -- max client
|
||||
local client_number = 0
|
||||
local CMD = setmetatable({}, { __gc = function() netpack.clear(queue) end })
|
||||
local nodelay = false
|
||||
|
||||
local connection = {}
|
||||
|
||||
function gateserver.openclient(fd)
|
||||
if connection[fd] then
|
||||
socketdriver.start(fd)
|
||||
end
|
||||
end
|
||||
|
||||
function gateserver.closeclient(fd)
|
||||
local c = connection[fd]
|
||||
if c then
|
||||
connection[fd] = false
|
||||
socketdriver.close(fd)
|
||||
end
|
||||
end
|
||||
|
||||
function gateserver.start(handler)
|
||||
assert(handler.message)
|
||||
assert(handler.connect)
|
||||
|
||||
function CMD.open( source, conf )
|
||||
assert(not socket)
|
||||
local address = conf.address or "0.0.0.0"
|
||||
local port = assert(conf.port)
|
||||
maxclient = conf.maxclient or 1024
|
||||
nodelay = conf.nodelay
|
||||
socket = socketdriver.listen(address, port)
|
||||
socketdriver.start(socket)
|
||||
if handler.open then
|
||||
return handler.open(source, conf)
|
||||
end
|
||||
end
|
||||
|
||||
function CMD.close()
|
||||
assert(socket)
|
||||
socketdriver.close(socket)
|
||||
socket = nil
|
||||
end
|
||||
|
||||
local MSG = {}
|
||||
|
||||
function MSG.data(fd, msg, sz)
|
||||
if connection[fd] then
|
||||
handler.message(fd, msg, sz)
|
||||
end
|
||||
end
|
||||
|
||||
function MSG.more()
|
||||
for fd, msg, sz in netpack.pop, queue do
|
||||
if connection[fd] then
|
||||
handler.message(fd, msg, sz)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MSG.open(fd, msg)
|
||||
if client_number >= maxclient then
|
||||
socketdriver.close(fd)
|
||||
return
|
||||
end
|
||||
if nodelay then
|
||||
socketdriver.nodelay(fd)
|
||||
end
|
||||
connection[fd] = true
|
||||
client_number = client_number + 1
|
||||
handler.connect(fd, msg)
|
||||
end
|
||||
|
||||
local function close_fd(fd)
|
||||
local c = connection[fd]
|
||||
if c ~= nil then
|
||||
connection[fd] = nil
|
||||
client_number = client_number - 1
|
||||
end
|
||||
end
|
||||
|
||||
function MSG.close(fd)
|
||||
if handler.disconnect then
|
||||
handler.disconnect(fd)
|
||||
end
|
||||
close_fd(fd)
|
||||
end
|
||||
|
||||
function MSG.error(fd, msg)
|
||||
if handler.error then
|
||||
handler.error(fd)
|
||||
end
|
||||
close_fd(fd)
|
||||
end
|
||||
|
||||
skynet.register_protocol {
|
||||
name = "socket",
|
||||
id = skynet.PTYPE_SOCKET, -- PTYPE_SOCKET = 6
|
||||
unpack = function ( msg, sz )
|
||||
return netpack.filter( queue, msg, sz)
|
||||
end,
|
||||
dispatch = function (_, _, q, type, ...)
|
||||
queue = q
|
||||
if type then
|
||||
MSG[type](...)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
skynet.start(function()
|
||||
skynet.dispatch("lua", function (_, address, cmd, ...)
|
||||
local f = CMD[cmd]
|
||||
if f then
|
||||
skynet.ret(skynet.pack(f(address, ...)))
|
||||
else
|
||||
skynet.ret(skynet.pack(handler.command(cmd, address, ...)))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return gateserver
|
||||
175
lualib/snax/loginserver.lua
Normal file
175
lualib/snax/loginserver.lua
Normal file
@@ -0,0 +1,175 @@
|
||||
local skynet = require "skynet"
|
||||
local socket = require "socket"
|
||||
local crypt = require "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) (A user defined method)
|
||||
|
||||
Error Code:
|
||||
400 Bad Request . challenge failed
|
||||
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 function write(fd, text)
|
||||
assert(socket.write(fd, text), "socket error")
|
||||
end
|
||||
|
||||
local function launch_slave(auth_handler)
|
||||
-- set socket buffer limit (8K)
|
||||
-- If the attacker send large package, close the socket
|
||||
socket.limit(8192)
|
||||
|
||||
local function auth(fd, addr)
|
||||
fd = assert(tonumber(fd))
|
||||
skynet.error(string.format("connect from %s (fd = %d)", addr, fd))
|
||||
socket.start(fd)
|
||||
local challenge = crypt.randomkey()
|
||||
write(fd, crypt.base64encode(challenge).."\n")
|
||||
|
||||
local handshake = assert(socket.readline(fd), "socket closed")
|
||||
local clientkey = crypt.base64decode(handshake)
|
||||
if #clientkey ~= 8 then
|
||||
error "Invalid client key"
|
||||
end
|
||||
local serverkey = crypt.randomkey()
|
||||
write(fd, crypt.base64encode(crypt.dhexchange(serverkey)).."\n")
|
||||
|
||||
local secret = crypt.dhsecret(clientkey, serverkey)
|
||||
|
||||
local response = assert(socket.readline(fd), "socket closed")
|
||||
local hmac = crypt.hmac64(challenge, secret)
|
||||
|
||||
if hmac ~= crypt.base64decode(response) then
|
||||
write(fd, "400 Bad Request\n")
|
||||
error "challenge failed"
|
||||
end
|
||||
|
||||
local etoken = assert(socket.readline(fd), "socket closed")
|
||||
|
||||
local token = crypt.desdecode(secret, crypt.base64decode(etoken))
|
||||
|
||||
local ok, server, uid = pcall(auth_handler,token)
|
||||
|
||||
socket.abandon(fd)
|
||||
return ok, server, uid, secret
|
||||
end
|
||||
|
||||
skynet.dispatch("lua", function(_,_,...)
|
||||
skynet.ret(skynet.pack(auth(...)))
|
||||
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)
|
||||
socket.start(fd)
|
||||
|
||||
if not ok then
|
||||
write(fd, "401 Unauthorized\n")
|
||||
error(server)
|
||||
end
|
||||
|
||||
if not conf.multilogin then
|
||||
if user_login[uid] then
|
||||
write(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(fd, "200 "..crypt.base64encode(err).."\n")
|
||||
else
|
||||
write(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, ...)
|
||||
if command == "register_slave" then
|
||||
table.insert(slave, source)
|
||||
skynet.ret(skynet.pack(nil))
|
||||
else
|
||||
skynet.ret(skynet.pack(conf.command_handler(command, ...)))
|
||||
end
|
||||
end)
|
||||
|
||||
for i=1,instance do
|
||||
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
|
||||
skynet.error(string.format("invalid client (fd = %d) error = %s", fd, err))
|
||||
end
|
||||
socket.close(fd)
|
||||
end)
|
||||
end
|
||||
|
||||
local function login(conf)
|
||||
local name = "." .. (conf.name or "login")
|
||||
skynet.start(function()
|
||||
local loginmaster = skynet.localname(name)
|
||||
if loginmaster then
|
||||
skynet.call(loginmaster, "lua", "register_slave")
|
||||
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
|
||||
299
lualib/snax/msgserver.lua
Normal file
299
lualib/snax/msgserver.lua
Normal file
@@ -0,0 +1,299 @@
|
||||
local skynet = require "skynet"
|
||||
local gateserver = require "snax.gateserver"
|
||||
local netpack = require "netpack"
|
||||
local crypt = require "crypt"
|
||||
local socketdriver = require "socketdriver"
|
||||
local assert = assert
|
||||
local b64encode = crypt.base64encode
|
||||
local b64decode = crypt.base64decode
|
||||
|
||||
--[[
|
||||
|
||||
Protocol:
|
||||
|
||||
All the number type is big-endian
|
||||
|
||||
Shakehands (The first package)
|
||||
|
||||
Client -> Server :
|
||||
|
||||
base64(uid)@base64(server)#base64(subid):index:base64(hmac)
|
||||
|
||||
Server -> Client
|
||||
|
||||
XXX ErrorCode
|
||||
404 User Not Found
|
||||
403 Index Expired
|
||||
401 Unauthorized
|
||||
400 Bad Request
|
||||
200 OK
|
||||
|
||||
Req-Resp
|
||||
|
||||
Client -> Server : Request
|
||||
word size (Not include self)
|
||||
string content (size-4)
|
||||
dword session
|
||||
|
||||
Server -> Client : Response
|
||||
word size (Not include self)
|
||||
string content (size-5)
|
||||
byte ok (1 is ok, 0 is error)
|
||||
dword session
|
||||
|
||||
API:
|
||||
server.userid(username)
|
||||
return uid, subid, server
|
||||
|
||||
server.username(uid, subid, server)
|
||||
return username
|
||||
|
||||
server.login(username, secret)
|
||||
update user secret
|
||||
|
||||
server.logout(username)
|
||||
user logout
|
||||
|
||||
server.ip(username)
|
||||
return ip when connection establish, or nil
|
||||
|
||||
server.start(conf)
|
||||
start server
|
||||
|
||||
Supported skynet command:
|
||||
kick username (may used by loginserver)
|
||||
login username secret (used by loginserver)
|
||||
logout username (used by agent)
|
||||
|
||||
Config for server.start:
|
||||
conf.expired_number : the number of the response message cached after sending out (default is 128)
|
||||
conf.login_handler(uid, secret) -> subid : the function when a new user login, alloc a subid for it. (may call by login server)
|
||||
conf.logout_handler(uid, subid) : the functon when a user logout. (may call by agent)
|
||||
conf.kick_handler(uid, subid) : the functon when a user logout. (may call by login server)
|
||||
conf.request_handler(username, session, msg, sz) : the function when recv a new request.
|
||||
conf.register_handler(servername) : call when gate open
|
||||
conf.disconnect_handler(username) : call when a connection disconnect (afk)
|
||||
]]
|
||||
|
||||
local server = {}
|
||||
|
||||
skynet.register_protocol {
|
||||
name = "client",
|
||||
id = skynet.PTYPE_CLIENT,
|
||||
}
|
||||
|
||||
local user_online = {}
|
||||
local handshake = {}
|
||||
local connection = {}
|
||||
|
||||
function server.userid(username)
|
||||
-- base64(uid)@base64(server)#base64(subid)
|
||||
local uid, servername, subid = username:match "([^@]*)@([^#]*)#(.*)"
|
||||
return b64decode(uid), b64decode(subid), b64decode(servername)
|
||||
end
|
||||
|
||||
function server.username(uid, subid, servername)
|
||||
return string.format("%s@%s#%s", b64encode(uid), b64encode(servername), b64encode(tostring(subid)))
|
||||
end
|
||||
|
||||
function server.logout(username)
|
||||
local u = user_online[username]
|
||||
user_online[username] = nil
|
||||
if u.fd then
|
||||
gateserver.closeclient(u.fd)
|
||||
connection[u.fd] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function server.login(username, secret)
|
||||
assert(user_online[username] == nil)
|
||||
user_online[username] = {
|
||||
secret = secret,
|
||||
version = 0,
|
||||
index = 0,
|
||||
username = username,
|
||||
response = {}, -- response cache
|
||||
}
|
||||
end
|
||||
|
||||
function server.ip(username)
|
||||
local u = user_online[username]
|
||||
if u and u.fd then
|
||||
return u.ip
|
||||
end
|
||||
end
|
||||
|
||||
function server.start(conf)
|
||||
local expired_number = conf.expired_number or 128
|
||||
|
||||
local handler = {}
|
||||
|
||||
local CMD = {
|
||||
login = assert(conf.login_handler),
|
||||
logout = assert(conf.logout_handler),
|
||||
kick = assert(conf.kick_handler),
|
||||
}
|
||||
|
||||
function handler.command(cmd, source, ...)
|
||||
local f = assert(CMD[cmd])
|
||||
return f(...)
|
||||
end
|
||||
|
||||
function handler.open(source, gateconf)
|
||||
local servername = assert(gateconf.servername)
|
||||
return conf.register_handler(servername)
|
||||
end
|
||||
|
||||
function handler.connect(fd, addr)
|
||||
handshake[fd] = addr
|
||||
gateserver.openclient(fd)
|
||||
end
|
||||
|
||||
function handler.disconnect(fd)
|
||||
handshake[fd] = nil
|
||||
local c = connection[fd]
|
||||
if c then
|
||||
c.fd = nil
|
||||
connection[fd] = nil
|
||||
if conf.disconnect_handler then
|
||||
conf.disconnect_handler(c.username)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
handler.error = handler.disconnect
|
||||
|
||||
-- atomic , no yield
|
||||
local function do_auth(fd, message, addr)
|
||||
local username, index, hmac = string.match(message, "([^:]*):([^:]*):([^:]*)")
|
||||
local u = user_online[username]
|
||||
if u == nil then
|
||||
return "404 User Not Found"
|
||||
end
|
||||
local idx = assert(tonumber(index))
|
||||
hmac = b64decode(hmac)
|
||||
|
||||
if idx <= u.version then
|
||||
return "403 Index Expired"
|
||||
end
|
||||
|
||||
local text = string.format("%s:%s", username, index)
|
||||
local v = crypt.hmac64(crypt.hashkey(text), u.secret)
|
||||
if v ~= hmac then
|
||||
return "401 Unauthorized"
|
||||
end
|
||||
|
||||
u.version = idx
|
||||
u.fd = fd
|
||||
u.ip = addr
|
||||
connection[fd] = u
|
||||
end
|
||||
|
||||
local function auth(fd, addr, msg, sz)
|
||||
local message = netpack.tostring(msg, sz)
|
||||
local ok, result = pcall(do_auth, fd, message, addr)
|
||||
if not ok then
|
||||
skynet.error(result)
|
||||
result = "400 Bad Request"
|
||||
end
|
||||
|
||||
local close = result ~= nil
|
||||
|
||||
if result == nil then
|
||||
result = "200 OK"
|
||||
end
|
||||
|
||||
socketdriver.send(fd, netpack.pack(result))
|
||||
|
||||
if close then
|
||||
gateserver.closeclient(fd)
|
||||
end
|
||||
end
|
||||
|
||||
local request_handler = assert(conf.request_handler)
|
||||
|
||||
-- u.response is a struct { message, version, index }
|
||||
local function retire_response(u)
|
||||
if u.index >= expired_number * 2 then
|
||||
local max = 0
|
||||
local response = u.response
|
||||
for k,p in pairs(response) do
|
||||
if p[3] < expired_number then
|
||||
response[k] = nil
|
||||
else
|
||||
p[3] = p[3] - expired_number
|
||||
if p[3] > max then
|
||||
max = p[3]
|
||||
end
|
||||
end
|
||||
end
|
||||
u.index = max + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function do_request(fd, msg, sz)
|
||||
local u = assert(connection[fd], "invalid fd")
|
||||
local msg_sz = sz - 4
|
||||
local session = netpack.tostring(msg, sz, msg_sz)
|
||||
local p = u.response[session]
|
||||
if p then
|
||||
-- session can be reuse in the same connection
|
||||
if p[2] == u.version then
|
||||
u.response[session] = nil
|
||||
p = nil
|
||||
end
|
||||
end
|
||||
|
||||
if p == nil then
|
||||
local ok, result = pcall(conf.request_handler, u.username, msg, msg_sz)
|
||||
-- NOTICE: YIELD here, socket may close.
|
||||
if not ok then
|
||||
skynet.error(result)
|
||||
result = "\0" .. session
|
||||
else
|
||||
result = result .. '\1' .. session
|
||||
end
|
||||
|
||||
p = { netpack.pack_string(result), u.version, u.index }
|
||||
if u.response[session] then
|
||||
skynet.error(string.format("Conflict session %s", crypt.hexencode(session)))
|
||||
end
|
||||
u.response[session] = p
|
||||
else
|
||||
netpack.tostring(msg, sz) -- request before, so free msg
|
||||
-- resend response, and update index p[3].
|
||||
p[3] = u.index
|
||||
end
|
||||
u.index = u.index + 1
|
||||
-- check connect again
|
||||
if connection[fd] then
|
||||
socketdriver.send(fd, p[1])
|
||||
end
|
||||
retire_response(u)
|
||||
end
|
||||
|
||||
local function request(fd, msg, sz)
|
||||
local ok, err = pcall(do_request, fd, msg, sz)
|
||||
-- not atomic, may yield
|
||||
if not ok then
|
||||
skynet.error(string.format("Invalid package %s : %s", err, netpack.tostring(msg, sz)))
|
||||
if connection[fd] then
|
||||
gateserver.closeclient(fd)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function handler.message(fd, msg, sz)
|
||||
local addr = handshake[fd]
|
||||
if addr then
|
||||
auth(fd,addr,msg,sz)
|
||||
handshake[fd] = nil
|
||||
else
|
||||
request(fd, msg, sz)
|
||||
end
|
||||
end
|
||||
|
||||
return gateserver.start(handler)
|
||||
end
|
||||
|
||||
return server
|
||||
Reference in New Issue
Block a user