diff --git a/channel_server.go b/channel_server.go index a72c0d0bc..f774e3bd6 100644 --- a/channel_server.go +++ b/channel_server.go @@ -3,28 +3,238 @@ package main import ( "encoding/hex" "fmt" + "io/ioutil" "net" "github.com/Andoryuuta/Erupe/network" "github.com/Andoryuuta/byteframe" ) +var loadDataCount int +var getPaperDataCount int + +func handlePacket(cc *network.CryptConn, pkt []byte) { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered from panic.") + } + }() + + bf := byteframe.NewByteFrameFromBytes(pkt) + opcode := network.PacketID(bf.ReadUint16()) + + if opcode == network.MSG_SYS_EXTEND_THRESHOLD { + opcode = network.PacketID(bf.ReadUint16()) + } + + switch opcode { + case network.MSG_SYS_PING: + ackHandle := bf.ReadUint32() + _ = bf.ReadUint16() + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_ACK)) + bfw.WriteUint32(ackHandle) + bfw.WriteUint32(0) + bfw.WriteUint32(0) + cc.SendPacket(bfw.Data()) + case network.MSG_SYS_TIME: + _ = bf.ReadUint8() + timestamp := bf.ReadUint32() // unix timestamp, e.g. 1577105879 + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_TIME)) + bfw.WriteUint8(0) + bfw.WriteUint32(timestamp) + cc.SendPacket(bfw.Data()) + case network.MSG_SYS_LOGIN: + ackHandle := bf.ReadUint32() + charID0 := bf.ReadUint32() + loginTokenNumber := bf.ReadUint32() + hardcodedZero0 := bf.ReadUint16() + requestVersion := bf.ReadUint16() + charID1 := bf.ReadUint32() + hardcodedZero1 := bf.ReadUint16() + loginTokenLength := bf.ReadUint16() // hardcoded to 0x11 + loginTokenString := bf.ReadBytes(17) + + _ = ackHandle + _ = charID0 + _ = loginTokenNumber + _ = hardcodedZero0 + _ = requestVersion + _ = charID1 + _ = hardcodedZero1 + _ = loginTokenLength + _ = loginTokenString + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_ACK)) + bfw.WriteUint32(ackHandle) + bfw.WriteUint64(0x000000005E00B9C2) // Timestamp? + cc.SendPacket(bfw.Data()) + + case network.MSG_MHF_ENUMERATE_EVENT: + fallthrough + case network.MSG_MHF_ENUMERATE_QUEST: + fallthrough + case network.MSG_MHF_ENUMERATE_RANKING: + fallthrough + case network.MSG_MHF_READ_MERCENARY_W: + fallthrough + case network.MSG_MHF_GET_ETC_POINTS: + fallthrough + case network.MSG_MHF_READ_GUILDCARD: + fallthrough + case network.MSG_MHF_READ_BEAT_LEVEL: + fallthrough + case network.MSG_MHF_GET_EARTH_STATUS: + fallthrough + case network.MSG_MHF_GET_EARTH_VALUE: + fallthrough + case network.MSG_MHF_GET_WEEKLY_SCHEDULE: + fallthrough + case network.MSG_MHF_LIST_MEMBER: + fallthrough + case network.MSG_MHF_LOAD_PLATE_DATA: + fallthrough + case network.MSG_MHF_LOAD_PLATE_BOX: + fallthrough + case network.MSG_MHF_LOAD_FAVORITE_QUEST: + fallthrough + case network.MSG_MHF_LOAD_PARTNER: + fallthrough + case network.MSG_MHF_GET_TOWER_INFO: + fallthrough + case network.MSG_MHF_LOAD_OTOMO_AIROU: + fallthrough + case network.MSG_MHF_LOAD_DECO_MYSET: + fallthrough + case network.MSG_MHF_LOAD_HUNTER_NAVI: + fallthrough + case network.MSG_MHF_GET_UD_SCHEDULE: + fallthrough + case network.MSG_MHF_GET_UD_INFO: + fallthrough + case network.MSG_MHF_GET_UD_MONSTER_POINT: + fallthrough + case network.MSG_MHF_GET_RAND_FROM_TABLE: + fallthrough + case network.MSG_MHF_ACQUIRE_MONTHLY_REWARD: + fallthrough + case network.MSG_MHF_GET_RENGOKU_RANKING_RANK: + fallthrough + case network.MSG_MHF_LOAD_PLATE_MYSET: + fallthrough + case network.MSG_MHF_LOAD_RENGOKU_DATA: + fallthrough + case network.MSG_MHF_ENUMERATE_SHOP: + fallthrough + case network.MSG_MHF_LOAD_SCENARIO_DATA: + fallthrough + case network.MSG_MHF_GET_BOOST_TIME_LIMIT: + fallthrough + case network.MSG_MHF_GET_BOOST_RIGHT: + fallthrough + case network.MSG_MHF_GET_REWARD_SONG: + fallthrough + case network.MSG_MHF_GET_GACHA_POINT: + fallthrough + case network.MSG_MHF_GET_KOURYOU_POINT: + fallthrough + case network.MSG_MHF_GET_ENHANCED_MINIDATA: + + ackHandle := bf.ReadUint32() + + data, err := ioutil.ReadFile(fmt.Sprintf("bin_resp/%s_resp.bin", opcode.String())) + if err != nil { + panic(err) + } + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_ACK)) + bfw.WriteUint32(ackHandle) + bfw.WriteBytes(data) + cc.SendPacket(bfw.Data()) + + case network.MSG_MHF_INFO_FESTA: + ackHandle := bf.ReadUint32() + _ = bf.ReadUint32() + + data, err := ioutil.ReadFile(fmt.Sprintf("bin_resp/%s_resp.bin", opcode.String())) + if err != nil { + panic(err) + } + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_ACK)) + bfw.WriteUint32(ackHandle) + bfw.WriteBytes(data) + cc.SendPacket(bfw.Data()) + + case network.MSG_MHF_LOADDATA: + ackHandle := bf.ReadUint32() + + data, err := ioutil.ReadFile(fmt.Sprintf("bin_resp/%s_resp%d.bin", opcode.String(), loadDataCount)) + if err != nil { + panic(err) + } + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_ACK)) + bfw.WriteUint32(ackHandle) + bfw.WriteBytes(data) + cc.SendPacket(bfw.Data()) + + loadDataCount++ + if loadDataCount > 1 { + loadDataCount = 0 + } + case network.MSG_MHF_GET_PAPER_DATA: + ackHandle := bf.ReadUint32() + + data, err := ioutil.ReadFile(fmt.Sprintf("bin_resp/%s_resp%d.bin", opcode.String(), getPaperDataCount)) + if err != nil { + panic(err) + } + + bfw := byteframe.NewByteFrame() + bfw.WriteUint16(uint16(network.MSG_SYS_ACK)) + bfw.WriteUint32(ackHandle) + bfw.WriteBytes(data) + cc.SendPacket(bfw.Data()) + + getPaperDataCount++ + if getPaperDataCount > 7 { + getPaperDataCount = 0 + } + default: + break + } + + fmt.Printf("Opcode: %s\n", opcode) + fmt.Printf("Data:\n%s\n", hex.Dump(pkt)) + + remainingData := bf.DataFromCurrent() + if len(remainingData) >= 2 && (opcode == network.MSG_SYS_TIME || opcode == network.MSG_MHF_INFO_FESTA) { + handlePacket(cc, remainingData) + } +} + func handleChannelServerConnection(conn net.Conn) { fmt.Println("Channel server got connection!") // Unlike the sign and entrance server, // the client DOES NOT initalize the channel connection with 8 NULL bytes. cc := network.NewCryptConn(conn) + for { pkt, err := cc.ReadPacket() if err != nil { return } - bf := byteframe.NewByteFrameFromBytes(pkt) - opcode := network.PacketID(bf.ReadUint16()) - fmt.Printf("Opcode: %s\n", opcode) - fmt.Printf("Data:\n%s\n", hex.Dump(pkt)) + handlePacket(cc, pkt) } } diff --git a/go.mod b/go.mod index 7db1bbc26..f8802b520 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,7 @@ require ( github.com/Andoryuuta/byteframe v0.0.0-20191219124302-41f4085eb4c0 github.com/BurntSushi/toml v0.3.1 github.com/golang-migrate/migrate v3.5.4+incompatible // indirect + github.com/gorilla/handlers v1.4.2 github.com/julienschmidt/httprouter v1.3.0 + github.com/lib/pq v1.3.0 ) diff --git a/go.sum b/go.sum index e7dcd6eee..a3ff7a999 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,14 @@ github.com/Andoryuuta/binio v0.0.0-20160731013325-2c89946fb8c3 h1:N8pCiqpJAHyOO8 github.com/Andoryuuta/binio v0.0.0-20160731013325-2c89946fb8c3/go.mod h1:4WK1jUpH8NFdDiv7IJcBfyCIOMqKjZ15kcw5eBKALvc= github.com/Andoryuuta/byteframe v0.0.0-20191219124302-41f4085eb4c0 h1:2pVgen9rh18IxSWxOa80bObcpyfrS6d5bJtZeCUN7rY= github.com/Andoryuuta/byteframe v0.0.0-20191219124302-41f4085eb4c0/go.mod h1:koVyx+gN3TfE70rpOidywETVODk87304YpwW69Y27J4= +github.com/Andoryuuta/erupe v0.0.0-20191219210047-7aef17f7d946 h1:Z20gk8dvCNRZuHEdeEyGVwbMs9IxyGs5gGU4zFN3aTs= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= diff --git a/launcher_server.go b/launcher_server.go index 5a1fe4ff0..1cbe2542a 100644 --- a/launcher_server.go +++ b/launcher_server.go @@ -1,8 +1,12 @@ package main import ( + "fmt" "net/http" + "net/http/httputil" + "os" + "github.com/gorilla/handlers" "github.com/julienschmidt/httprouter" ) @@ -11,15 +15,27 @@ func g6Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { } +func serverUniqueName(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + dump, err := httputil.DumpRequest(r, true) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + return + } + fmt.Println(string(dump)) + + fmt.Fprintf(w, `OK`) +} + // serveLauncherHTML is responsible for serving the launcher HTML and (HACK) serverlist.xml. func serveLauncherHTML(listenAddr string) { // Manually route the folder root to index.html? Is there a better way to do this? router := httprouter.New() router.GET("/g6_launcher/", g6Index) + router.GET("/server/unique.php", serverUniqueName) static := httprouter.New() static.ServeFiles("/*filepath", http.Dir("www")) router.NotFound = static - http.ListenAndServe(listenAddr, router) + http.ListenAndServe(listenAddr, handlers.LoggingHandler(os.Stdout, router)) } diff --git a/main.go b/main.go index cbbc8ce92..839196155 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,43 @@ package main import ( + "database/sql" "fmt" "time" + + "github.com/Andoryuuta/Erupe/signserver" + _ "github.com/lib/pq" ) func main() { fmt.Println("Starting!") + // Load the config.toml configuration. + // TODO(Andoryuuta): implement config loading. + + // Create the postgres DB pool. + db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres password=admin dbname=erupe sslmode=disable") + if err != nil { + panic(err) + } + + // Test the DB connection. + err = db.Ping() + if err != nil { + panic(err) + } + + // Finally start our server(s). go serveLauncherHTML(":80") go doEntranceServer(":53310") - go doSignServer(":53312") + + signServer := signserver.NewServer( + &signserver.Config{ + DB: db, + ListenAddr: ":53312", + }) + go signServer.Listen() + go doChannelServer(":54001") for { diff --git a/sign_server.go b/signserver/dsgn_resp.go similarity index 68% rename from sign_server.go rename to signserver/dsgn_resp.go index aee31cc32..d0bbedf95 100644 --- a/sign_server.go +++ b/signserver/dsgn_resp.go @@ -1,27 +1,6 @@ -package main +package signserver -import ( - "fmt" - "io" - "net" - - "github.com/Andoryuuta/Erupe/network" - "github.com/Andoryuuta/byteframe" -) - -/* -var conf *config.Config - -func init() { - c, err := config.LoadConfig("config.toml") - if err != nil { - panic(err) - } - - conf = c - -} -*/ +import "github.com/Andoryuuta/byteframe" func paddedString(x string, size uint) []byte { out := make([]byte, size) @@ -42,6 +21,12 @@ func uint16PascalString(bf *byteframe.ByteFrame, x string) { bf.WriteNullTerminatedBytes([]byte(x)) } +func makeSignInFailureResp(respID RespID) []byte { + bf := byteframe.NewByteFrame() + bf.WriteUint8(uint8(respID)) + return bf.Data() +} + func makeSignInResp(username string) []byte { bf := byteframe.NewByteFrame() @@ -51,7 +36,7 @@ func makeSignInResp(username string) []byte { bf.WriteUint8(1) // resp_code bf.WriteUint8(0) // file/patch server count - bf.WriteUint8(1) // entrance server count + bf.WriteUint8(4) // entrance server count bf.WriteUint8(1) // character count bf.WriteUint32(0xFFFFFFFF) // login_token_number bf.WriteBytes(paddedString("logintokenstrng", 16)) // login_token (16 byte padded string) @@ -61,6 +46,9 @@ func makeSignInResp(username string) []byte { // Array(this.entrance_server_count, PascalString(Byte, "utf8")), uint8PascalString(bf, "localhost:53310") + uint8PascalString(bf, "") + uint8PascalString(bf, "") + uint8PascalString(bf, "mhf-n.capcom.com.tw") /////////////////////////// // Characters: @@ -87,6 +75,8 @@ func makeSignInResp(username string) []byte { bf.WriteUint32(469153291) // character ID 469153291 bf.WriteUint16(30) // Exp, HR[x] is split by 0, 1, 30, 50, 99, 299, 998, 999 + //44.204 + /* 0=大劍/Big sword 1=重弩/Heavy crossbow @@ -108,7 +98,7 @@ func makeSignInResp(username string) []byte { bf.WriteUint32(1576761172) // Last login date, unix timestamp in seconds. bf.WriteUint8(1) // Sex, 0=male, 1=female. - bf.WriteUint8(0) // Is new character, 1 replaces character name with ?????. + bf.WriteUint8(1) // Is new character, 1 replaces character name with ?????. grMode := uint8(0) bf.WriteUint8(1) // GR level if grMode == 0 bf.WriteUint8(grMode) // GR mode. @@ -145,51 +135,3 @@ func makeSignInResp(username string) []byte { return bf.Data() } - -func handleSignServerConnection(conn net.Conn) { - // Client initalizes the connection with a one-time buffer of 8 NULL bytes. - nullInit := make([]byte, 8) - n, err := io.ReadFull(conn, nullInit) - if err != nil { - fmt.Println(err) - return - } else if n != len(nullInit) { - fmt.Println("io.ReadFull couldn't read the full 8 byte init.") - return - } - - cc := network.NewCryptConn(conn) - for { - pkt, err := cc.ReadPacket() - if err != nil { - return - } - - bf := byteframe.NewByteFrameFromBytes(pkt) - loginType := string(bf.ReadNullTerminatedBytes()) - username := string(bf.ReadNullTerminatedBytes()) - password := string(bf.ReadNullTerminatedBytes()) - unk := string(bf.ReadNullTerminatedBytes()) - fmt.Printf("Got signin, type: %s, username: %s, password %s, unk: %s", loginType, username, password, unk) - - resp := makeSignInResp(username) - cc.SendPacket(resp) - - } -} - -func doSignServer(listenAddr string) { - l, err := net.Listen("tcp", listenAddr) - if err != nil { - panic(err) - } - defer l.Close() - - for { - conn, err := l.Accept() - if err != nil { - panic(err) - } - go handleSignServerConnection(conn) - } -} diff --git a/signserver/respid.go b/signserver/respid.go new file mode 100644 index 000000000..a55aed14a --- /dev/null +++ b/signserver/respid.go @@ -0,0 +1,50 @@ +package signserver + +//revive:disable +type RespID uint16 + +//go:generate stringer -type=RespID +const ( + SIGN_UNKNOWN RespID = iota + SIGN_SUCCESS + SIGN_EFAILED // Authentication server communication failed + SIGN_EILLEGAL // Incorrect input, authentication has been suspended + SIGN_EALERT // Authentication server process error + SIGN_EABORT // The internal procedure of the authentication server ended abnormally + SIGN_ERESPONSE // Procedure terminated due to abnormal certification report + SIGN_EDATABASE // Database connection failed + SIGN_EABSENCE + SIGN_ERESIGN + SIGN_ESUSPEND_D + SIGN_ELOCK + SIGN_EPASS + SIGN_ERIGHT + SIGN_EAUTH + SIGN_ESUSPEND // This account is temporarily suspended. Please contact customer service for details + SIGN_EELIMINATE // This account is permanently suspended. Please contact customer service for details + SIGN_ECLOSE + SIGN_ECLOSE_EX // Login process is congested.
Please try to sign in again later + SIGN_EINTERVAL + SIGN_EMOVED + SIGN_ENOTREADY + SIGN_EALREADY + SIGN_EIPADDR // Region block because of IP address. + SIGN_EHANGAME + SIGN_UPD_ONLY + SIGN_EMBID + SIGN_ECOGCODE + SIGN_ETOKEN + SIGN_ECOGLINK + SIGN_EMAINTE + SIGN_EMAINTE_NOUPDATE + + // Couldn't find names for the following: + UNK_32 + UNK_33 + UNK_34 + UNK_35 + + SIGN_XBRESPONSE + SIGN_EPSI + SIGN_EMBID_PSI +) diff --git a/signserver/session.go b/signserver/session.go new file mode 100644 index 000000000..235b6020b --- /dev/null +++ b/signserver/session.go @@ -0,0 +1,107 @@ +package signserver + +import ( + "database/sql" + "encoding/hex" + "fmt" + "net" + "sync" + + "github.com/Andoryuuta/Erupe/network" + "github.com/Andoryuuta/byteframe" +) + +// Session holds state for the sign server connection. +type Session struct { + sync.Mutex + sid int + server *Server + rawConn *net.Conn + cryptConn *network.CryptConn +} + +func (session *Session) fail() { + session.server.Lock() + delete(session.server.sessions, session.sid) + session.server.Unlock() + +} + +func (session *Session) work() { + for { + pkt, err := session.cryptConn.ReadPacket() + if err != nil { + session.fail() + return + } + + err = session.handlePacket(pkt) + if err != nil { + session.fail() + return + } + } +} + +func (session *Session) handlePacket(pkt []byte) error { + bf := byteframe.NewByteFrameFromBytes(pkt) + reqType := string(bf.ReadNullTerminatedBytes()) + switch reqType { + case "DSGN:100": + session.handleDSGNRequest(bf) + break + case "DELETE:100": + loginTokenString := string(bf.ReadNullTerminatedBytes()) + _ = loginTokenString + characterID := bf.ReadUint32() + + fmt.Printf("Got delete request for character ID: %v\n", characterID) + fmt.Printf("remaining unknown data:\n%s\n", hex.Dump(bf.DataFromCurrent())) + default: + fmt.Printf("Got unknown request type %s, data:\n%s\n", reqType, hex.Dump(bf.DataFromCurrent())) + } + + return nil +} + +func (session *Session) handleDSGNRequest(bf *byteframe.ByteFrame) error { + reqUsername := string(bf.ReadNullTerminatedBytes()) + reqPassword := string(bf.ReadNullTerminatedBytes()) + reqUnk := string(bf.ReadNullTerminatedBytes()) + fmt.Printf("Got sign in request:\n\tUsername: %s\n\tPassword %s\n\tUnk: %s\n", reqUsername, reqPassword, reqUnk) + + // TODO(Andoryuuta): remove plaintext password storage if this ever becomes more than a toy project. + var ( + id int + password string + ) + err := session.server.db.QueryRow("SELECT id, password FROM users WHERE username = $1", reqUsername).Scan(&id, &password) + var serverRespBytes []byte + switch { + case err == sql.ErrNoRows: + fmt.Printf("No rows for username %s\n", reqUsername) + serverRespBytes = makeSignInFailureResp(SIGN_EAUTH) + break + case err != nil: + serverRespBytes = makeSignInFailureResp(SIGN_EABORT) + fmt.Println("Got error on SQL query!") + fmt.Println(err) + break + default: + if reqPassword == password { + fmt.Println("Passwords match!") + serverRespBytes = makeSignInResp(reqUsername) + } else { + fmt.Println("Passwords don't match!") + serverRespBytes = makeSignInFailureResp(SIGN_EPASS) + } + + } + + err = session.cryptConn.SendPacket(serverRespBytes) + if err != nil { + return err + } + + return nil +} diff --git a/signserver/sign_server.go b/signserver/sign_server.go new file mode 100644 index 000000000..17704f6b2 --- /dev/null +++ b/signserver/sign_server.go @@ -0,0 +1,79 @@ +package signserver + +import ( + "database/sql" + "fmt" + "io" + "net" + "sync" + + "github.com/Andoryuuta/Erupe/network" +) + +// Config struct allows configuring the server. +type Config struct { + DB *sql.DB + ListenAddr string +} + +// Server is a MHF sign server. +type Server struct { + sync.Mutex + sid int + sessions map[int]*Session + db *sql.DB + listenAddr string +} + +// NewServer creates a new Server type. +func NewServer(config *Config) *Server { + s := &Server{ + sid: 0, + sessions: make(map[int]*Session), + db: config.DB, + listenAddr: config.ListenAddr, + } + return s +} + +// Listen listens for new connections and accepts/serves them. +func (s *Server) Listen() { + l, err := net.Listen("tcp", s.listenAddr) + if err != nil { + panic(err) + } + defer l.Close() + + for { + conn, err := l.Accept() + if err != nil { + panic(err) + } + + go s.handleConnection(s.sid, conn) + s.sid++ + } +} + +func (s *Server) handleConnection(sid int, conn net.Conn) { + // Client initalizes the connection with a one-time buffer of 8 NULL bytes. + nullInit := make([]byte, 8) + _, err := io.ReadFull(conn, nullInit) + if err != nil { + fmt.Println(err) + conn.Close() + return + } + + session := &Session{ + server: s, + rawConn: &conn, + cryptConn: network.NewCryptConn(conn), + } + + s.Lock() + s.sessions[sid] = session + s.Unlock() + + session.work() +}