├── tests ├── config.nims ├── setting.json ├── test_server_boot.nim ├── test_server_send.nim ├── test_server_ping.nim ├── test_server_close.nim ├── test_server_receive.nim └── test_utility.nim ├── images ├── 001.png ├── 002.gif ├── 003.png └── 004.gif ├── .gitignore ├── example ├── chat_example │ ├── setting.json │ ├── chat_server.nim │ └── chat_client.html ├── echo_example │ ├── setting.json │ ├── echo_server.nim │ └── echo_client.html └── room_chat_example │ ├── setting.json │ ├── room_chat_server.nim │ └── room_chat_client.html ├── bamboo_websocket.nimble ├── bamboo_websocket ├── errors.nim ├── websocket.nim ├── frame.nim ├── private │ └── utilities.nim └── bamboo_websocket.nim ├── README_ja.md └── README.md /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../bamboo_websocket") -------------------------------------------------------------------------------- /images/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obemaru4012/bamboo_websocket/HEAD/images/001.png -------------------------------------------------------------------------------- /images/002.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obemaru4012/bamboo_websocket/HEAD/images/002.gif -------------------------------------------------------------------------------- /images/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obemaru4012/bamboo_websocket/HEAD/images/003.png -------------------------------------------------------------------------------- /images/004.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obemaru4012/bamboo_websocket/HEAD/images/004.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | develop_memo.txt 3 | commit_message.txt 4 | 5 | chat_server 6 | echo_server 7 | room_chat_server -------------------------------------------------------------------------------- /tests/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "websocket_version" : "13", 3 | "upgrade": "websocket", 4 | "connection": "upgrade", 5 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 6 | } -------------------------------------------------------------------------------- /example/chat_example/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "websocket_version": "13", 3 | "upgrade": "websocket", 4 | "connection": "upgrade", 5 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 6 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 7 | "mask_key_seeder": "514902776" 8 | } 9 | -------------------------------------------------------------------------------- /example/echo_example/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "websocket_version": "13", 3 | "upgrade": "websocket", 4 | "connection": "upgrade", 5 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 6 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 7 | "mask_key_seeder": "514902776" 8 | } 9 | -------------------------------------------------------------------------------- /example/room_chat_example/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "websocket_version": "13", 3 | "upgrade": "websocket", 4 | "connection": "upgrade", 5 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 6 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 7 | "mask_key_seeder": "514902776" 8 | } 9 | -------------------------------------------------------------------------------- /bamboo_websocket.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.3.3" 4 | author = "obemaru4012" 5 | description = "This is a simple implementation of a WebSocket server with 100% Nim." 6 | license = "MIT" 7 | skipDirs = @["tests", "images"] 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 1.4.8" 12 | -------------------------------------------------------------------------------- /bamboo_websocket/errors.nim: -------------------------------------------------------------------------------- 1 | ##[ 2 | errors.nim 3 | 4 | ]## 5 | type 6 | NoMaskedFrameReceiveError* = object of ValueError 7 | ServerSettingNotEnoughError* = object of KeyError 8 | WebSocketHandShakeSubProtcolsProcedureError* = object of CatchableError 9 | WebSocketHandShakeHeaderError* = object of ValueError 10 | WebSocketDataReceivedPostProcessError* = object of CatchableError 11 | UnknownOpcodeReceiveError* = object of ValueError 12 | WebSocketOtherError* = object of IOError -------------------------------------------------------------------------------- /bamboo_websocket/websocket.nim: -------------------------------------------------------------------------------- 1 | import asyncnet 2 | import tables 3 | 4 | type 5 | # 接続ステータス 6 | ConnectionStatus* = enum 7 | INITIAl = 0 # 初期状態 8 | CONNECTING = 1 # 接続中... 9 | OPEN = 2 # 接続済 10 | CLOSING = 3 # 切断中... 11 | CLOSED = 4 # 切断済 12 | 13 | # 制御フレーム(0x8, 0x, 0xa)とデータフレーム(0x1, 0x2) 14 | OpCode* = enum 15 | CONTINUATION = 0x0 # 継続フレーム 16 | TEXT = 0x1 # テキストフレーム 17 | BINARY = 0x2 # バイナリフレーム 18 | CLOSE = 0x8 # クローズフレーム 19 | PING = 0x9 # ピン! 20 | PONG = 0xa # ポン! 21 | 22 | # WebSocketオブジェクト 23 | WebSocket* = ref object 24 | id*: string ## 接続ID(OID) 25 | socket*: AsyncSocket ## 接続本体 26 | status*: ConnectionStatus ## 接続ステータス 27 | sec_websocket_accept*: string ## Sec-WebSocket-Accept 28 | sec_websocket_protocol*: seq[string] ## Sec-WebSocket-Protocol 29 | version*: string ## バージョン(現行は13) 30 | upgrade*: string ## Upgradeリクエスト 31 | connection*: string ## Connectionリクエスト 32 | optional_data*: Table[string, string] ## 追加情報(接続名などを追加の情報をTable保持) -------------------------------------------------------------------------------- /tests/test_server_boot.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | import unittest 8 | import asyncdispatch, 9 | asynchttpserver, 10 | httpcore, 11 | json, 12 | nativesockets, 13 | net, 14 | strutils, 15 | uri 16 | 17 | from websocket import WebSocket 18 | from bamboo_websocket import 19 | handshake, 20 | openWebSocket, 21 | receiveMessage, 22 | sendMessage 23 | 24 | # ダミー設定テーブル作成 25 | var setting = parseJson("""{"websocket_version": "13","upgrade": "websocket","connection": "upgrade","websocket_key": "dGhlIHNhbXBsZSBub25jZQ==","magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11","mask_key_seeder": "514902776"}""") 26 | 27 | proc bootCallBack(request: Request) {.async, gcsafe.} = 28 | var ws = WebSocket() 29 | if request.url.path == "/": 30 | try: 31 | ws = await openWebSocket(request, setting) 32 | except: 33 | discard 34 | 35 | suite "server boot": 36 | 37 | test "boot server": 38 | var server = newAsyncHttpServer() 39 | asyncCheck server.serve(Port(9001), bootCallBack, "localhost") 40 | server.close() 41 | assert true -------------------------------------------------------------------------------- /example/echo_example/echo_server.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, 2 | asynchttpserver, 3 | asyncnet, 4 | httpcore, 5 | nativesockets, 6 | net, 7 | strutils, 8 | uri 9 | 10 | from bamboo_websocket/websocket import WebSocket, ConnectionStatus, OpCode 11 | from bamboo_websocket/bamboo_websocket import loadServerSetting, openWebSocket, receiveMessage, sendMessage 12 | 13 | proc callBack(request: Request) {.async, gcsafe.} = 14 | var ws: WebSocket 15 | var setting = loadServerSetting() 16 | 17 | if request.url.path == "/": 18 | let headers = {"Content-type": "text/html; charset=utf-8"} 19 | let content = readFile("./echo_client.html") 20 | await request.respond(Http200, content, headers.newHttpHeaders()) 21 | 22 | if request.url.path == "/chat": 23 | try: 24 | ws = await openWebSocket(request, setting) 25 | except: 26 | let message = getCurrentException() 27 | 28 | while ws.status == ConnectionStatus.OPEN: 29 | try: 30 | let receive = await ws.receiveMessage() 31 | 32 | if receive[0] == OpCode.TEXT: 33 | echo("ID: ", ws.id, " echo back.") 34 | await ws.sendMessage(receive[1], 0x1, 3000, true) 35 | 36 | if receive[0] == OpCode.CLOSE: 37 | echo("ID: ", ws.id, " has Closed.") 38 | break 39 | 40 | except: 41 | ws.status = ConnectionStatus.CLOSED 42 | ws.socket.close() 43 | 44 | ws.socket.close() 45 | 46 | if isMainModule: 47 | var server = newAsyncHttpServer() 48 | waitFor server.serve(Port(9001), callBack) -------------------------------------------------------------------------------- /tests/test_server_send.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | import unittest 8 | import asyncdispatch, 9 | asynchttpserver, 10 | httpcore, 11 | json, 12 | nativesockets, 13 | net, 14 | strutils, 15 | uri 16 | 17 | from websocket import WebSocket 18 | from bamboo_websocket import 19 | handshake, 20 | openWebSocket, 21 | receiveMessage, 22 | sendMessage 23 | 24 | # ダミー設定テーブル作成 25 | var setting = parseJson("""{"websocket_version": "13","upgrade": "websocket","connection": "upgrade","websocket_key": "dGhlIHNhbXBsZSBub25jZQ==","magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11","mask_key_seeder": "514902776"}""") 26 | 27 | proc sendMessageCallBack(request: Request) {.async, gcsafe.} = 28 | var ws = WebSocket() 29 | if request.url.path == "/": 30 | try: 31 | ws = await openWebSocket(request, setting) 32 | waitFor ws.sendMessage("ぶんぶんぶんなぐり!", 0x1, 1000, true) 33 | 34 | except: 35 | discard 36 | 37 | suite "send message": 38 | 39 | test "send message": 40 | var server = newAsyncHttpServer() 41 | asyncCheck server.serve(Port(9001), sendMessageCallBack, "localhost") 42 | var ws = waitFor handshake("localhost", 80, 9001, setting) 43 | let receive = waitFor ws.receiveMessage() 44 | server.close() 45 | check $receive[1] == "ぶんぶんぶんなぐり!" -------------------------------------------------------------------------------- /tests/test_server_ping.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | import unittest 8 | import asyncdispatch, 9 | asynchttpserver, 10 | httpcore, 11 | json, 12 | nativesockets, 13 | net, 14 | strutils, 15 | uri 16 | 17 | from websocket import WebSocket, OpCode 18 | from bamboo_websocket import 19 | handshake, 20 | openWebSocket, 21 | receiveMessage, 22 | sendMessage 23 | 24 | # ダミー設定テーブル作成 25 | var setting = parseJson("""{"websocket_version": "13","upgrade": "websocket","connection": "upgrade","websocket_key": "dGhlIHNhbXBsZSBub25jZQ==","magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11","mask_key_seeder": "514902776"}""") 26 | 27 | proc sendMessageCallBack(request: Request) {.async, gcsafe.} = 28 | var ws = WebSocket() 29 | var message {.global.} : tuple[opcode: Opcode, message: string] 30 | if request.url.path == "/": 31 | try: 32 | ws = await openWebSocket(request, setting) 33 | message = await ws.receiveMessage() 34 | except: 35 | discard 36 | 37 | suite "receive ping": 38 | 39 | test "receive ping": 40 | var server = newAsyncHttpServer() 41 | asyncCheck server.serve(Port(9001), sendMessageCallBack, "localhost") 42 | var ws = waitFor handshake("localhost", 80, 9001, setting) 43 | waitFor ws.sendMessage("", 0x9, 1000, true) 44 | var r = waitFor ws.receiveMessage() 45 | check r[0] == OpCode.PONG 46 | 47 | -------------------------------------------------------------------------------- /tests/test_server_close.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | import unittest 8 | import asyncdispatch, 9 | asynchttpserver, 10 | httpcore, 11 | json, 12 | nativesockets, 13 | net, 14 | strutils, 15 | uri 16 | 17 | from websocket import WebSocket, OpCode 18 | from bamboo_websocket import 19 | handshake, 20 | openWebSocket, 21 | receiveMessage, 22 | sendMessage 23 | 24 | # ダミー設定テーブル作成 25 | var setting = parseJson("""{"websocket_version": "13","upgrade": "websocket","connection": "upgrade","websocket_key": "dGhlIHNhbXBsZSBub25jZQ==","magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11","mask_key_seeder": "514902776"}""") 26 | 27 | proc sendMessageCallBack(request: Request) {.async, gcsafe.} = 28 | var ws = WebSocket() 29 | var message {.global.} : tuple[opcode: Opcode, message: string] 30 | if request.url.path == "/": 31 | try: 32 | ws = await openWebSocket(request, setting) 33 | message = await ws.receiveMessage() 34 | except: 35 | discard 36 | 37 | suite "receive close": 38 | 39 | test "receive close": 40 | var server = newAsyncHttpServer() 41 | asyncCheck server.serve(Port(9001), sendMessageCallBack, "localhost") 42 | var ws = waitFor handshake("localhost", 80, 9001, setting) 43 | waitFor ws.sendMessage("", 0x8, 1000, true) 44 | var r = waitFor ws.receiveMessage() 45 | check r[0] == OpCode.CLOSE 46 | 47 | -------------------------------------------------------------------------------- /tests/test_server_receive.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | import unittest 8 | import asyncdispatch, 9 | asynchttpserver, 10 | httpcore, 11 | json, 12 | nativesockets, 13 | net, 14 | strutils, 15 | uri 16 | 17 | from websocket import WebSocket, Opcode 18 | from bamboo_websocket import 19 | handshake, 20 | openWebSocket, 21 | receiveMessage, 22 | sendMessage 23 | 24 | # ダミー設定テーブル作成 25 | var setting = parseJson("""{"websocket_version": "13","upgrade": "websocket","connection": "upgrade","websocket_key": "dGhlIHNhbXBsZSBub25jZQ==","magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11","mask_key_seeder": "514902776"}""") 26 | 27 | proc sendMessageCallBack(request: Request) {.async, gcsafe.} = 28 | var ws = WebSocket() 29 | var message {.global.} : tuple[opcode: Opcode, message: string] 30 | if request.url.path == "/": 31 | try: 32 | ws = await openWebSocket(request, setting) 33 | message = await ws.receiveMessage() 34 | await ws.sendMessage(message[1], 0x1, 1000, true) 35 | except: 36 | discard 37 | 38 | suite "receive message": 39 | 40 | test "receive message": 41 | var server = newAsyncHttpServer() 42 | asyncCheck server.serve(Port(9001), sendMessageCallBack, "localhost") 43 | var ws = waitFor handshake("localhost", 80, 9001, setting) 44 | waitFor ws.sendMessage("ぶんぶんぶんなぐり!", 0x1, 1000, true) 45 | var r = waitFor ws.receiveMessage() 46 | check r[1] == "ぶんぶんぶんなぐり!" 47 | 48 | -------------------------------------------------------------------------------- /bamboo_websocket/frame.nim: -------------------------------------------------------------------------------- 1 | ##[ 2 | fram.nim 3 | 4 | Frame format: 5 | ​​ 6 | 0 1 2 3 7 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 8 | +-+-+-+-+-------+-+-------------+-------------------------------+ 9 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 10 | |I|S|S|S| (4) |A| (7) | (16/64) | 11 | |N|V|V|V| |S| | (if payload len==126/127) | 12 | | |1|2|3| |K| | | 13 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 14 | | Extended payload length continued, if payload len == 127 | 15 | + - - - - - - - - - - - - - - - +-------------------------------+ 16 | | |Masking-key, if MASK set to 1 | 17 | +-------------------------------+-------------------------------+ 18 | | Masking-key (continued) | Payload Data | 19 | +-------------------------------- - - - - - - - - - - - - - - - + 20 | : Payload Data continued ... : 21 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 22 | | Payload Data continued ... | 23 | +---------------------------------------------------------------+ 24 | ]## 25 | from websocket import OpCode 26 | 27 | type 28 | # フレームオブジェクト 29 | Frame* = object 30 | # 受信継続のためにnil許容とする 31 | FIN*: bool # メッセージのお尻かどうかをチェック(0: お尻じゃない, 1: お尻) 32 | RSV1*: bool # 基本0で0以外が受信されたら「通信を閉じるべし!」 33 | RSV2*: bool # 基本0で0以外が受信されたら「通信を閉じるべし!」 34 | RSV3*: bool # 基本0で0以外が受信されたら「通信を閉じるべし!」 35 | OPCODE*: Opcode # ペイロードがどんなものかを定義 36 | MASK*: bool # データがマスクされているかチェック「1」の場合はペイロード内にマスク用キーがあるのでそれを利用する 37 | # C => Sの場合は基本「1」であることが約束されている 38 | PAYLOAD_LENGTH*: int8 # 0 〜 125 => それが長さ, 39 | # 126 => 後続の2バイトが16bit無符号整数に解釈されてペイロードの長さになる 40 | # 127 => 後続の8バイトが64bit無符号整数(最上位bitは0でなければならない)に解釈されてペイロードの長さになる 41 | MASK_KEY*: string # マスクキー 42 | DATA*: string # ペイロードデータ -------------------------------------------------------------------------------- /tests/test_utility.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | include ./private/utilities 8 | 9 | import unittest 10 | import base64, 11 | json, 12 | nativesockets, 13 | net, 14 | std/sha1 15 | 16 | # ダミー設定テーブル作成 17 | var setting = parseJson("""{"websocket_version": "13","upgrade": "websocket","connection": "upgrade","websocket_key": "dGhlIHNhbXBsZSBub25jZQ==","magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11","mask_key_seeder": "514902776"}""") 18 | 19 | suite "utilities test": 20 | 21 | test "convertBinaryStrings('P'): string": 22 | check convertBinaryStrings('P') == "01010000" 23 | 24 | test "convertBinaryStrings('A'): string": 25 | check convertBinaryStrings('A') == "01000001" 26 | 27 | test "convertBinaryStrings('N'): string": 28 | check convertBinaryStrings('N') == "01001110" 29 | 30 | test "convertBinaryStrings('D'): string": 31 | check convertBinaryStrings('D') == "01000100" 32 | 33 | test "convertBinaryStrings('A'): string": 34 | check convertBinaryStrings('A') == "01000001" 35 | 36 | test "fourBitFromChar('y'): int": 37 | check fourBitFromChar('y') == 34 38 | 39 | test "decodeHexStrings(dGhlIHNhbXBsZSBub25jZQ==): string": 40 | var sec_websocket_accept: string = $(sha1.secureHash("dGhlIHNhbXBsZSBub25jZQ==" & "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) 41 | sec_websocket_accept = base64.encode(decodeHexStrings(sec_websocket_accept)) 42 | check sec_websocket_accept == "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" 43 | 44 | test "convertRuneSequence(ぷーゆーゆ!ぷーゆーゆ!ぷーゆーゆ!)": 45 | check convertRuneSequence("ぷーゆーゆ!ぷーゆーゆ!ぷーゆーゆ!", 8) == @["ぷーゆーゆ!ぷー", "ゆーゆ!ぷーゆー", "ゆ!"] 46 | 47 | test "convertRuneSequence(ゆめちゃん、ぷゆゆ、ぺゆゆ、天才、ぱゆゆ、サムス、ホッティ)": 48 | check convertRuneSequence("ゆめちゃん、ぷゆゆ、ぺゆゆ、天才、ぱゆゆ、サムス、ホッティ", 4) == @["ゆめちゃ", "ん、ぷゆ", "ゆ、ぺゆ", "ゆ、天才", "、ぱゆゆ", "、サムス", "、ホッテ", "ィ"] 49 | 50 | test "generateMaskKey(1)": 51 | check generateMaskKey(1) == @['j', '`', '}', '\x84'] 52 | 53 | test "generateMaskKey(514902776)": 54 | check generateMaskKey(514902776) == @['\x82', '\x00', ']', '\x8D'] -------------------------------------------------------------------------------- /example/chat_example/chat_server.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, 2 | asynchttpserver, 3 | asyncnet, 4 | httpcore, 5 | json, 6 | nativesockets, 7 | net, 8 | strutils, 9 | tables, 10 | uri 11 | 12 | from bamboo_websocket/websocket import WebSocket, ConnectionStatus, OpCode 13 | from bamboo_websocket/bamboo_websocket import loadServerSetting, openWebSocket, receiveMessage, sendMessage 14 | 15 | var WebSockets: seq[WebSocket] = newSeq[WebSocket]() 16 | 17 | proc callBack(request: Request) {.async, gcsafe.} = 18 | 19 | var ws = WebSocket() 20 | var setting = loadServerSetting() 21 | 22 | if request.url.path == "/": 23 | let headers = {"Content-type": "text/html; charset=utf-8"} 24 | let content = readFile("./chat_client.html") 25 | await request.respond(Http200, content, headers.newHttpHeaders()) 26 | 27 | if request.url.path == "/chat": 28 | try: 29 | ws = await openWebSocket(request, setting) 30 | 31 | # SubProtocol Proc(名前を取得、さらにURI decode処理を追加) 32 | var sub_protocol = decodeUrl(request.headers["sec-websocket-protocol", 0]) 33 | ws.optional_data["name"] = $(sub_protocol) 34 | 35 | WebSockets.add(ws) 36 | echo("ID: ", ws.id, ", Tag: ", ws.optional_data["name"], " has Opened.") 37 | except: 38 | let message = getCurrentException() 39 | echo(message.msg) 40 | 41 | ws.status = ConnectionStatus.INITIAl 42 | 43 | while ws.status == ConnectionStatus.OPEN: 44 | try: 45 | let receive = await ws.receiveMessage() 46 | if receive[0] == OpCode.TEXT: 47 | var message: string = $(%* [{"name": ws.optional_data["name"], "message": receive[1]}]) 48 | for websocket in WebSockets: 49 | if websocket.id != ws.id: 50 | echo("$# => $#" % [$(ws.id), $(websocket.id)]) 51 | await websocket.sendMessage(message, 0x1) 52 | 53 | if receive[0] == OpCode.CLOSE: 54 | echo("ID: ", ws.id, " has Closed.") 55 | break 56 | 57 | except: 58 | ws.status = ConnectionStatus.CLOSED 59 | ws.socket.close() 60 | 61 | if WebSockets.find(ws) != -1: 62 | WebSockets.delete(WebSockets.find(ws)) 63 | 64 | if ws.status == ConnectionStatus.OPEN: 65 | ws.socket.close() 66 | 67 | if isMainModule: 68 | var server = newAsyncHttpServer() 69 | waitFor server.serve(Port(9001), callBack) -------------------------------------------------------------------------------- /bamboo_websocket/private/utilities.nim: -------------------------------------------------------------------------------- 1 | import mersenne, random, strutils, unicode 2 | 3 | proc convertBinaryStrings*(bytes: char): string = 4 | ##[ 5 | 数字を2進数型の文字列に変換する 6 | EX: 255 => "111111111" 7 | ]## 8 | return bytes.BiggestInt.toBin(8) 9 | 10 | proc convertBinaryStrings*(bytes: int32): string = 11 | ##[ 12 | 数字を2進数型の文字列に変換する 13 | EX: 10 => "00001010" 14 | ]## 15 | return bytes.toBin(8) 16 | 17 | proc convertBinaryStrings*(bytes: int64): string = 18 | ##[ 19 | 数字を2進数型の文字列に変換する 20 | EX: 255 => "111111111" 21 | ]## 22 | return bytes.toBin(8) 23 | 24 | proc fourBitFromChar(c: char): int = 25 | ##[ 26 | 文字から4bitに変換 27 | EX: a => 10, b => 11 28 | ]## 29 | if c in "0123456789": 30 | return ord(c) - ord('0') 31 | 32 | if c in "abcdefghijklmnopqrstuvwxyz": 33 | return ord(c) - ord('a') + 10 34 | 35 | if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": 36 | return ord(c) - ord('A') + 10 37 | 38 | return 0 39 | 40 | proc decodeHexStrings*(hex: string): string = 41 | ##[ 42 | 16進数の形の文字列をデコードする 43 | ]## 44 | var byte_strings :seq[string] = newSeq[string](hex.len() div 2) 45 | for i in countup(0, byte_strings.len() - 1): 46 | byte_strings[i] = $(chr((fourBitFromChar(hex[2 * i]) shl 4) or fourBitFromChar(hex[2 * i + 1]))) 47 | return byte_strings.join("") 48 | 49 | proc convertRuneSequence*(message: string, per_length: int=3000): seq[string] = 50 | ##[ 51 | StringsをRune文字列に変換しつつ、per_lengthごとの文字数に分割する。 52 | 全部日本語で1MBはおおよそ33万文字なので、デフォルトは10KB文字分ごとぐらいにしておく。 53 | ]## 54 | var message_to_rune = toRunes(message) 55 | 56 | var count: int = message_to_rune.len() div per_length 57 | var modulo: int = message_to_rune.len() mod per_length 58 | 59 | var messages: seq[string] 60 | var left: int = 0 61 | var right: int = per_length 62 | for n in countup(0, count): 63 | if n == count: 64 | # echo(left, "..<", left + modulo, ": ", message_to_rune[left.. 0: 66 | messages.add($message_to_rune[left.. $#" % [$(ws.id), $(websocket.id)]) 65 | await websocket.sendMessage(message, 0x1) 66 | 67 | if receive[0] == OpCode.CLOSE: 68 | # 接続を閉じたむねを部屋の他の人に通知 69 | for websocket in WebSockets[ws.optional_data["room"]]: 70 | if websocket.id != ws.id: 71 | await websocket.sendMessage($(%* [{"name": ws.optional_data["name"], "closing": "true"}]), 0x1) 72 | 73 | echo("ID: ", ws.id, " has Closed.") 74 | break 75 | 76 | except: 77 | ws.status = ConnectionStatus.CLOSED 78 | ws.socket.close() 79 | 80 | # 接続をCloseするClientの所属する部屋 81 | var room: string = ws.optional_data["room"] 82 | try: 83 | if WebSockets[room].find(ws) != -1: 84 | WebSockets[room].delete(WebSockets[room].find(ws)) 85 | 86 | if ws.status == ConnectionStatus.OPEN: 87 | ws.socket.close() 88 | except: 89 | # 変な部屋番号を持っているClientは無視する。 90 | discard 91 | 92 | if isMainModule: 93 | var server = newAsyncHttpServer() 94 | waitFor server.serve(Port(9001), callBack) -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | #### 🐼Bamboo-WebSocket🌿 2 | 3 | ![FLgp3pCakAAG1JU](https://user-images.githubusercontent.com/88951380/158893548-13a50cea-92ff-4506-acb8-202e5e5e317e.png) 4 | 5 | - 100%Nim による WebSocket サーバーのシンプルな実装です。 6 | - 竹を割ったようにさっぱりとした実装を目指しています。 7 | - チャットサーバー、ゲーム用のサーバーを簡単に作成できることを目指しています。 8 | - Bamboo の詳細な説明と利用方法については wiki に記載予定です。 9 | - 最新バージョンは**0.3.3**になります。 10 | - [README in English.](https://github.com/obemaru4012/bamboo_websocket/blob/master/README.md) 11 | 12 | #### 🖥Dependency 13 | 14 | `requires "nim >= 1.4.8"` 15 | 16 | #### 👩‍💻Setup 17 | 18 | ```bash 19 | nimble install bamboowebsocket@0.3.3 20 | ``` 21 | 22 | #### 🤔Description 23 | 24 | - Nim 標準で提供されている[asynchttpserver](https://nim-lang.org/docs/asynchttpserver.html)での利用を想定しています。 25 | 26 | #### 🤙Usage 27 | 28 | ##### 🐥Echo Server 29 | 30 | - 以下は、クライアントから受信したメッセージをエコーするサーバーです。 31 | - 以下のコードは bamboowebsocket/example/echo_example ディレクトリ内に置いてあります。 32 | 33 | ```nim 34 | # echo_server.nim 35 | 36 | import asyncdispatch, 37 | asynchttpserver, 38 | asyncnet, 39 | httpcore, 40 | nativesockets, 41 | net, 42 | strutils, 43 | uri 44 | 45 | from bamboo_websocket/websocket import WebSocket, ConnectionStatus, OpCode 46 | from bamboo_websocket/bamboo_websocket import loadServerSetting, openWebSocket, receiveMessage, sendMessage 47 | 48 | proc callBack(request: Request) {.async, gcsafe.} = 49 | var ws: WebSocket 50 | var setting = loadServerSetting() 51 | 52 | if request.url.path == "/": 53 | let headers = {"Content-type": "text/html; charset=utf-8"} 54 | let content = readFile("./echo_client.html") 55 | await request.respond(Http200, content, headers.newHttpHeaders()) 56 | 57 | if request.url.path == "/chat": 58 | try: 59 | ws = await openWebSocket(request, setting) 60 | except: 61 | let message = getCurrentException() 62 | 63 | while ws.status == ConnectionStatus.OPEN: 64 | try: 65 | let receive = await ws.receiveMessage() 66 | 67 | if receive[0] == OpCode.TEXT: 68 | echo("ID: ", ws.id, " echo back.") 69 | await ws.sendMessage(receive[1], 0x1, 3000, true) 70 | 71 | if receive[0] == OpCode.CLOSE: 72 | echo("ID: ", ws.id, " has Closed.") 73 | break 74 | 75 | except: 76 | ws.status = ConnectionStatus.CLOSED 77 | ws.socket.close() 78 | 79 | ws.socket.close() 80 | 81 | if isMainModule: 82 | var server = newAsyncHttpServer() 83 | waitFor server.serve(Port(9001), callBack) 84 | 85 | ``` 86 | 87 | - サーバー用の設定ファイルを記述する json ファイルをサーバーファイル(ehco_server.nim 等)と同じ場所に配置する必要があります。 88 | 89 | ```json 90 | { 91 | "websocket_version": "13", 92 | "upgrade": "websocket", 93 | "connection": "upgrade", 94 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 95 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 96 | "mask_key_seeder": "514902776" 97 | } 98 | ``` 99 | 100 | - ディレクトリ配置は以下の画像のようになります。 101 | ![001](https://user-images.githubusercontent.com/88951380/165452751-9cb833f9-2214-4ea6-bde0-1818e1127d57.png) 102 | 103 | - echo_server.nim をコンパイル後に実行します。 104 | 105 | ```bash 106 | nim c -r echo_server.nim 107 | ``` 108 | 109 | ![002](https://user-images.githubusercontent.com/88951380/165452764-32cb29a6-a2e3-42f9-a5a5-5926d57a462a.gif) 110 | 111 | #### 😏Advanced Usage 112 | 113 | ##### 🐄Chat Server 114 | 115 | - 以下は、各クライアント間でのチャットを実現するサーバーです。 116 | - 以下のコードは bamboowebsocket/example/chat_example ディレクトリ内に置いてあります。 117 | 118 | ```nim 119 | # chat_server.nim 120 | 121 | import asyncdispatch, 122 | asynchttpserver, 123 | asyncnet, 124 | httpcore, 125 | json, 126 | nativesockets, 127 | net, 128 | strutils, 129 | tables, 130 | uri 131 | 132 | from bamboo_websocket/websocket import WebSocket, ConnectionStatus, OpCode 133 | from bamboo_websocket/bamboo_websocket import loadServerSetting, openWebSocket, receiveMessage, sendMessage 134 | 135 | var WebSockets: seq[WebSocket] = newSeq[WebSocket]() 136 | 137 | proc callBack(request: Request) {.async, gcsafe.} = 138 | 139 | var ws = WebSocket() 140 | var setting = loadServerSetting() 141 | 142 | if request.url.path == "/": 143 | let headers = {"Content-type": "text/html; charset=utf-8"} 144 | let content = readFile("./chat_client.html") 145 | await request.respond(Http200, content, headers.newHttpHeaders()) 146 | 147 | if request.url.path == "/chat": 148 | try: 149 | ws = await openWebSocket(request, setting) 150 | 151 | # SubProtocol Proc(名前を取得、さらにURI decode処理を追加) 152 | var sub_protocol = decodeUrl(request.headers["sec-websocket-protocol", 0]) 153 | ws.optional_data["name"] = $(sub_protocol) 154 | 155 | WebSockets.add(ws) 156 | echo("ID: ", ws.id, ", Tag: ", ws.optional_data["name"], " has Opened.") 157 | except: 158 | let message = getCurrentException() 159 | echo(message.msg) 160 | 161 | ws.status = ConnectionStatus.INITIAl 162 | 163 | while ws.status == ConnectionStatus.OPEN: 164 | try: 165 | let receive = await ws.receiveMessage() 166 | if receive[0] == OpCode.TEXT: 167 | var message: string = $(%* [{"name": ws.optional_data["name"], "message": receive[1]}]) 168 | for websocket in WebSockets: 169 | if websocket.id != ws.id: 170 | echo("$# => $#" % [$(ws.id), $(websocket.id)]) 171 | await websocket.sendMessage(message, 0x1) 172 | 173 | if receive[0] == OpCode.CLOSE: 174 | echo("ID: ", ws.id, " has Closed.") 175 | break 176 | 177 | except: 178 | ws.status = ConnectionStatus.CLOSED 179 | ws.socket.close() 180 | 181 | if WebSockets.find(ws) != -1: 182 | WebSockets.delete(WebSockets.find(ws)) 183 | 184 | if ws.status == ConnectionStatus.OPEN: 185 | ws.socket.close() 186 | 187 | if isMainModule: 188 | var server = newAsyncHttpServer() 189 | waitFor server.serve(Port(9001), callBack) 190 | 191 | ``` 192 | 193 | - サーバー用の設定ファイルを記述する json ファイル(setting.json)をサーバーファイル(chat_server.nim 等)と同じ場所に配置する必要があります。 194 | 195 | ```json 196 | { 197 | "websocket_version": "13", 198 | "upgrade": "websocket", 199 | "connection": "upgrade", 200 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 201 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 202 | "mask_key_seeder": "514902776" 203 | } 204 | ``` 205 | 206 | - chat_server.nim をコンパイル後に実行します。 207 | 208 | ```bash 209 | nim c -r chat_server.nim 210 | ``` 211 | 212 | ![004](https://user-images.githubusercontent.com/88951380/173271545-15a22b29-7825-4b16-944e-ba1bc92b92ee.gif) 213 | 214 | ##### 🐭Game Server 215 | 216 | [TODO] 217 | 218 | #### 📝Author 219 | 220 | - [obemaru4012](https://github.com/obemaru4012) 221 | 222 | #### 📖References 223 | 224 | - [RFC 6455 - The WebSocket Protocol (日本語訳)](https://triple-underscore.github.io/RFC6455-ja.html) 225 | - [WebSocket サーバーの記述 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_servers) 226 | - [WebSocket クライアントアプリケーションの記述 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications) 227 | - [Nim Package Directory](https://nimble.directory/pkg/bamboowebsocket) 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 🐼Bamboo-WebSocket🌿 2 | 3 | ![FLgp3pCakAAG1JU](https://user-images.githubusercontent.com/88951380/158893548-13a50cea-92ff-4506-acb8-202e5e5e317e.png) 4 | 5 | - This is a lightweight WebSocket server implemented entirely in Nim. 6 | - Our goal is to create a clean and elegant implementation, inspired by the simplicity of splitting bamboo. 7 | - This project aims to simplify the creation of chat and gaming servers. 8 | - [TODO] Detailed documentation about Bamboo and its usage will be provided in the wiki. 9 | - The latest release is **0.3.3**. 10 | - [README in Japanese.](https://github.com/obemaru4012/bamboo_websocket/blob/master/README_ja.md) 11 | 12 | #### 🖥Dependency 13 | 14 | `requires "nim >= 1.4.8"` 15 | 16 | #### 👩‍💻Setup 17 | 18 | ```bash 19 | nimble install bamboowebsocket@0.3.3 20 | ``` 21 | 22 | #### 🤔Description 23 | 24 | - It is intended to be used with [asynchttpserver](https://nim-lang.org/docs/asynchttpserver.html), which is provided in the Nim standard. 25 | 26 | #### 🤙Usage 27 | 28 | ##### 🐥Echo Server 29 | 30 | - The following is a server that echoes messages received from clients. 31 | - The following code can be found in the bamboowebsocket/example/echo_example directory. 32 | 33 | ```nim 34 | # echo_server.nim 35 | 36 | import asyncdispatch, 37 | asynchttpserver, 38 | asyncnet, 39 | httpcore, 40 | nativesockets, 41 | net, 42 | strutils, 43 | uri 44 | 45 | from bamboo_websocket/websocket import WebSocket, ConnectionStatus, OpCode 46 | from bamboo_websocket/bamboo_websocket import loadServerSetting, openWebSocket, receiveMessage, sendMessage 47 | 48 | proc callBack(request: Request) {.async, gcsafe.} = 49 | var ws: WebSocket 50 | var setting = loadServerSetting() 51 | 52 | if request.url.path == "/": 53 | let headers = {"Content-type": "text/html; charset=utf-8"} 54 | let content = readFile("./echo_client.html") 55 | await request.respond(Http200, content, headers.newHttpHeaders()) 56 | 57 | if request.url.path == "/chat": 58 | try: 59 | ws = await openWebSocket(request, setting) 60 | except: 61 | let message = getCurrentException() 62 | 63 | while ws.status == ConnectionStatus.OPEN: 64 | try: 65 | let receive = await ws.receiveMessage() 66 | 67 | if receive[0] == OpCode.TEXT: 68 | echo("ID: ", ws.id, " echo back.") 69 | await ws.sendMessage(receive[1], 0x1, 3000, true) 70 | 71 | if receive[0] == OpCode.CLOSE: 72 | echo("ID: ", ws.id, " has Closed.") 73 | break 74 | 75 | except: 76 | ws.status = ConnectionStatus.CLOSED 77 | ws.socket.close() 78 | 79 | ws.socket.close() 80 | 81 | if isMainModule: 82 | var server = newAsyncHttpServer() 83 | waitFor server.serve(Port(9001), callBack) 84 | 85 | ``` 86 | 87 | - A json file describing the configuration file for the server must be placed in the same location as the server file (e.g. ehco_server.nim)。 88 | 89 | ```json 90 | { 91 | "websocket_version": "13", 92 | "upgrade": "websocket", 93 | "connection": "upgrade", 94 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 95 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 96 | "mask_key_seeder": "514902776" 97 | } 98 | ``` 99 | 100 | - Run echo_server.nim after compilation. 101 | 102 | ```bash 103 | nim c -r echo_server.nim 104 | ``` 105 | 106 | ![002](https://user-images.githubusercontent.com/88951380/165452764-32cb29a6-a2e3-42f9-a5a5-5926d57a462a.gif) 107 | 108 | #### 😏Advanced Usage 109 | 110 | ##### 🐄Chat Server 111 | 112 | - The following code is a server that enables chatting between each client. 113 | - The following code can be found in the bamboowebsocket/example/chat_example directory. 114 | 115 | ```nim 116 | # chat_server.nim 117 | 118 | import asyncdispatch, 119 | asynchttpserver, 120 | asyncnet, 121 | httpcore, 122 | json, 123 | nativesockets, 124 | net, 125 | strutils, 126 | tables, 127 | uri 128 | 129 | from bamboo_websocket/websocket import WebSocket, ConnectionStatus, OpCode 130 | from bamboo_websocket/bamboo_websocket import loadServerSetting, openWebSocket, receiveMessage, sendMessage 131 | 132 | var WebSockets: seq[WebSocket] = newSeq[WebSocket]() 133 | 134 | proc callBack(request: Request) {.async, gcsafe.} = 135 | 136 | var ws = WebSocket() 137 | var setting = loadServerSetting() 138 | 139 | if request.url.path == "/": 140 | let headers = {"Content-type": "text/html; charset=utf-8"} 141 | let content = readFile("./chat_client.html") 142 | await request.respond(Http200, content, headers.newHttpHeaders()) 143 | 144 | if request.url.path == "/chat": 145 | try: 146 | ws = await openWebSocket(request, setting) 147 | 148 | # SubProtocol Proc(名前を取得、さらにURI decode処理を追加) 149 | var sub_protocol = decodeUrl(request.headers["sec-websocket-protocol", 0]) 150 | ws.optional_data["name"] = $(sub_protocol) 151 | 152 | WebSockets.add(ws) 153 | echo("ID: ", ws.id, ", Tag: ", ws.optional_data["name"], " has Opened.") 154 | except: 155 | let message = getCurrentException() 156 | echo(message.msg) 157 | 158 | ws.status = ConnectionStatus.INITIAl 159 | 160 | while ws.status == ConnectionStatus.OPEN: 161 | try: 162 | let receive = await ws.receiveMessage() 163 | if receive[0] == OpCode.TEXT: 164 | var message: string = $(%* [{"name": ws.optional_data["name"], "message": receive[1]}]) 165 | for websocket in WebSockets: 166 | if websocket.id != ws.id: 167 | echo("$# => $#" % [$(ws.id), $(websocket.id)]) 168 | await websocket.sendMessage(message, 0x1) 169 | 170 | if receive[0] == OpCode.CLOSE: 171 | echo("ID: ", ws.id, " has Closed.") 172 | break 173 | 174 | except: 175 | ws.status = ConnectionStatus.CLOSED 176 | ws.socket.close() 177 | 178 | if WebSockets.find(ws) != -1: 179 | WebSockets.delete(WebSockets.find(ws)) 180 | 181 | if ws.status == ConnectionStatus.OPEN: 182 | ws.socket.close() 183 | 184 | if isMainModule: 185 | var server = newAsyncHttpServer() 186 | waitFor server.serve(Port(9001), callBack) 187 | 188 | ``` 189 | 190 | - A json file(setting.json) describing the configuration file for the server must be placed in the same location as the server file (e.g. chat_server.nim)。 191 | 192 | ```json 193 | { 194 | "websocket_version": "13", 195 | "upgrade": "websocket", 196 | "connection": "upgrade", 197 | "websocket_key": "dGhlIHNhbXBsZSBub25jZQ==", 198 | "magic_strings": "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 199 | "mask_key_seeder": "514902776" 200 | } 201 | ``` 202 | 203 | - Run chat_server.nim after compilation. 204 | 205 | ```bash 206 | nim c -r chat_server.nim 207 | ``` 208 | 209 | ![004](https://user-images.githubusercontent.com/88951380/173271545-15a22b29-7825-4b16-944e-ba1bc92b92ee.gif) 210 | 211 | ##### 🐭Game Server 212 | 213 | [TODO] 214 | 215 | #### 📝Author 216 | 217 | - [obemaru4012](https://github.com/obemaru4012) 218 | 219 | #### 📖References 220 | 221 | - [RFC 6455 - The WebSocket Protocol (日本語訳)](https://triple-underscore.github.io/RFC6455-ja.html) 222 | - [WebSocket サーバーの記述 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_servers) 223 | - [WebSocket クライアントアプリケーションの記述 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications) 224 | - [Nim Package Directory](https://nimble.directory/pkg/bamboowebsocket) 225 | -------------------------------------------------------------------------------- /example/echo_example/echo_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bamboo websocket echo chat 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 28 |
29 | 30 |
31 |

Welcome to Bamboo WebSocket Sample Echo Chat

32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 | 44 |
45 |
46 | 47 | 50 | 53 | 56 | 57 | 75 | 76 | 96 | 97 | 116 | 117 | 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /example/chat_example/chat_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bamboo websocket chat 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 28 |
29 | 30 |
31 |

Welcome to Bamboo WebSocket Sample Chat

32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 | 42 |
43 |
44 | 45 | 48 | 51 | 54 | 55 | 73 | 74 | 94 | 95 | 115 | 116 | 146 | 147 | 326 | 327 | 328 | 329 | -------------------------------------------------------------------------------- /example/room_chat_example/room_chat_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Chat 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 28 |
29 | 30 |
31 |

Welcome to Bamboo WebSocket Sample Room Chat

32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 | 42 |
43 |
44 | 45 | 48 | 51 | 54 | 55 | 73 | 74 | 94 | 95 | 115 | 116 | 148 | 149 | 358 | 359 | 360 | 361 | -------------------------------------------------------------------------------- /bamboo_websocket/bamboo_websocket.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, 2 | asynchttpserver, 3 | asyncnet, 4 | base64, 5 | httpclient, 6 | httpcore, 7 | json, 8 | nativesockets, 9 | net, 10 | oids, 11 | sequtils, 12 | std/sets, 13 | std/sha1, 14 | strutils, 15 | streams, 16 | tables 17 | 18 | from errors import 19 | NoMaskedFrameReceiveError, 20 | ServerSettingNotEnoughError, 21 | UnknownOpCodeReceiveError, 22 | WebSocketHandShakeSubProtcolsProcedureError, 23 | WebSocketDataReceivedPostProcessError, 24 | WebSocketHandShakeHeaderError, 25 | WebSocketOtherError 26 | 27 | from frame import Frame 28 | from websocket import WebSocket, ConnectionStatus, OpCode 29 | from ./private/utilities import 30 | convertBinaryStrings, 31 | decodeHexStrings, 32 | convertRuneSequence, 33 | generateMaskKey 34 | 35 | proc handshake*(host: string, client_port: int, server_port: int, setting: JsonNode): Future[WebSocket] {.async.} = 36 | ##[ 37 | 38 | ]## 39 | var ws = WebSocket() 40 | var client = newAsyncHttpClient() 41 | client.headers = newHttpHeaders({ 42 | "Host": "$#:$#" % [host, $(client_port)], 43 | "Origin": "http://$#:$#" % [host, $(client_port)], 44 | "Upgrade": "$#" % [setting["upgrade"].getStr()], 45 | "Connection": "$#" % [setting["connection"].getStr()], 46 | "Sec-WebSocket-Key": "$#" % [setting["websocket_key"].getStr()], 47 | "Sec-WebSocket-Version": "13" 48 | }) 49 | 50 | # [TODO] HTTP"S" 51 | try: 52 | var response = await client.get("http://$#:$#/" % [host, $(server_port)]) 53 | except: 54 | # [TODO] raise error. 55 | ws.socket = nil 56 | ws.status = ConnectionStatus.INITIAl 57 | return ws 58 | 59 | ws.socket = client.getSocket() 60 | ws.status = ConnectionStatus.OPEN 61 | return ws 62 | 63 | proc getSecWebSocketAccept(sec_webSocket_key: string, magic_strings="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"): string = 64 | ##[ 65 | リクエストヘッダーの「Sec-WebSocket-Key」から「Sec-WebSocket-Accept」を作成する 66 | ]## 67 | var sec_websocket_accept: string = $(sha1.secureHash(sec_webSocket_key & magic_strings)) 68 | sec_websocket_accept = base64.encode(decodeHexStrings(sec_websocket_accept)) 69 | return $(sec_webSocket_accept) 70 | 71 | proc getHeaderValues(header: HttpHeaders, key: string, lower: bool=true): seq[string] = 72 | ##[ 73 | NimのHttpHeaderはKeyにかぶりがあると先頭の値をデフォルトで取得するため、被りがあっても全件取得を行う 74 | ]## 75 | var headers: seq[string] = @[] 76 | var start: int = 0 77 | while true: 78 | try: 79 | var value: string = header[key, start] 80 | if lower: 81 | headers.add(value.toLower()) 82 | else: 83 | # そのまま値を取ってくる 84 | headers.add(value) 85 | start += 1 86 | except IndexDefect: 87 | # 取得できなくなるまで 88 | break 89 | 90 | return headers 91 | 92 | proc loadServerSetting*(path="./setting.json"): JsonNode = 93 | ##[ 94 | 設定ファイルを読み込む 95 | ]## 96 | let settings = parseFile(path) 97 | return settings 98 | 99 | proc checkServerSetting*(settings: JsonNode, required_keys: seq[string], ): bool = 100 | ##[ 101 | 設定ファイルに最低限必要な設定が存在するかをチェック 102 | すなわち「required_keys」が実際の「settings」のsubsetである。 103 | ]## 104 | var keys: seq[string] 105 | for key in settings.keys(): 106 | keys.add(key) 107 | 108 | return required_keys.toHashSet() <= keys.toHashSet() 109 | 110 | proc openWebSocket*(request: Request, 111 | setting: JsonNode, 112 | ): Future[WebSocket] {.async.} = 113 | ##[ 114 | 115 | ]## 116 | var ws = WebSocket( 117 | id: "", 118 | socket: nil, 119 | status: ConnectionStatus.INITIAl, 120 | sec_websocket_accept: "", 121 | sec_websocket_protocol: @[], 122 | version: "", 123 | upgrade: "", 124 | connection: "", 125 | optional_data: initTable[string, string]() 126 | ) 127 | 128 | # server setting fileのチェック 129 | var r: bool = checkServerSetting(setting, ["websocket_version", "upgrade", "connection", "websocket_key", "magic_strings", "mask_key_seeder"].toSeq()) 130 | if r == false: 131 | raise newException(ServerSettingNotEnoughError, "設定ファイルの設定が不足しています。設定ファイルを見直してください。") 132 | 133 | # ハンドシェイク開始 134 | ws.status = ConnectionStatus.CONNECTING 135 | 136 | # ハンドシェイクリクエスト解析 137 | var headers: HttpHeaders = request.headers 138 | 139 | # Sec-WebSocket-Version: 現行の「13」ではない場合× 140 | if not request.headers.hasKey("sec-websocket-version"): 141 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダーに「sec-websocket-version」が見つかりません。") 142 | 143 | var sec_websocket_version = getHeaderValues(request.headers, "sec-websocket-version") 144 | 145 | if not all(sec_websocket_version, proc (x: string): bool = x == setting["websocket_version"].getStr()): 146 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダー「sec-websocket-version」の値が$#ではありません。($#)" % [setting["websocket_version"].getStr(), headers["sec-websocket-version"]]) 147 | 148 | let version = sec_websocket_version[sec_websocket_version.low()] 149 | ws.version = version 150 | 151 | # Upgrade: 「websocket」が含まれない場合× 152 | if not request.headers.hasKey("Upgrade") : 153 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダーに「Upgrade」が見つかりません。") 154 | 155 | var upgrades = getHeaderValues(request.headers, "Upgrade") 156 | var upgrade_checker = proc (x: string): bool = x == setting["upgrade"].getStr() 157 | if not any(upgrades, upgrade_checker): 158 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダー「Upgrade」の値が$#ではありません。($#)" % [setting["upgrade"].getStr(), headers["Upgrade"]]) 159 | 160 | let upgrade = setting["upgrade"].getStr() 161 | ws.upgrade = upgrade 162 | 163 | # Connection: 「upgrade」が含まれない場合× 164 | if not request.headers.hasKey("Connection") : 165 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダーに「Connection」が見つかりません。") 166 | 167 | var connections = getHeaderValues(request.headers, "Connection") 168 | var connections_checker = proc (x: string): bool = x == setting["connection"].getStr() 169 | if not any(connections, connections_checker): 170 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダー「Connection」の値が$#ではありません。($#)" % [setting["connection"].getStr(), headers["Connection"]]) 171 | 172 | let connection = setting["connection"].getStr() 173 | ws.connection = connection 174 | 175 | # Sec-WebSocket-Key: 存在しない場合× 176 | if not request.headers.hasKey("Sec-WebSocket-Key"): 177 | raise newException(WebSocketHandShakeHeaderError, "WebSocketハンドシェイクリクエストヘッダーに「Sec-WebSocket-Key」が見つかりません。") 178 | 179 | var sec_websocket_accept: string = getSecWebSocketAccept($request.headers["Sec-WebSocket-Key"].strip(), setting["magic_strings"].getStr()) 180 | ws.sec_websocket_accept = sec_websocket_accept 181 | 182 | # レスポンス用ソケット取得 183 | ws.socket = request.client 184 | # レスポンスヘッダー文字列作成 185 | var response = "HTTP/1.1 101 Switching Protocols" & "\n" 186 | response.add("Sec-WebSocket-Accept: " & sec_websocket_accept & "\n") 187 | response.add("Connection: " & connection & "\n") 188 | response.add("Upgrade: " & upgrade & "\n") 189 | 190 | # Sec-WebSocket-Protocol: が存在する場合(なくてもよい) 191 | # ここに送付される値によってサーバの種類をハンドルする。 192 | if request.headers.hasKey("Sec-WebSocket-Protocol"): 193 | var sub_protocol = getHeaderValues(request.headers, "Sec-WebSocket-Protocol", lower=false) 194 | response.add("Sec-Websocket-Protocol: " & sub_protocol[sub_protocol.low()] & "\n") 195 | 196 | ws.sec_websocket_protocol = sub_protocol 197 | 198 | # Sec-WebSocket-Protocolの値に基づいて独自の処理を行う 199 | # if not ws.subProtocolProcess(request): 200 | # raise newException(WebSocketHandShakeSubProtcolsProcedureError, "WebSocketハンドシェイク時の独自処理(subProtcolsProc)でエラーが発生しています。") 201 | 202 | # お尻に改行コード追加 203 | response.add("\n") 204 | 205 | # ハンドシェイク 206 | try: 207 | await ws.socket.send(response) 208 | except: 209 | raise newException(WebSocketOtherError, "WebSocketハンドシェイクレスポンスの送信でエラーが発生しています。") 210 | 211 | ws.id = $(genOid()) 212 | ws.status = ConnectionStatus.OPEN # ここでOPENに設定しておき、「newAsyncHttpServer」で回す 213 | 214 | return ws 215 | 216 | proc sendMessage*(ws: WebSocket, message: string, opcode: uint8, per_length: int=3000, is_masked: bool=false): Future[void] {.async.} = 217 | ##[ 218 | 219 | ]## 220 | # 1回に送信するデータ量ごとに分割する。デフォルトはおおよそ10KB分の文字数。 221 | var messages: seq[string] 222 | messages = convertRuneSequence(message, per_length) 223 | 224 | if ws.status != ConnectionStatus.OPEN: 225 | raise newException(WebSocketOtherError, "端点との接続が確立されていません。") 226 | 227 | let length = messages.len() - 1 228 | for index, m in messages: 229 | var frame = Frame() 230 | # 一発で送信される場合 231 | if length == 0: 232 | frame.FIN = true # 一発終了 233 | frame.RSV1 = false # 基本0 234 | frame.RSV2 = false # 基本0 235 | frame.RSV3 = false # 基本0 236 | frame.OPCODE = OpCode(opcode) 237 | frame.MASK = is_masked # SERVER => CLIENTの場合はFalse、CLIENT => SERVERの場合はTrueが基本線 238 | frame.DATA = m # 送信データ 239 | 240 | # 分割して送信される場合(初回) 241 | elif length > 0 and index == 0: 242 | frame.FIN = false # 一発終了 243 | frame.RSV1 = false # 基本0 244 | frame.RSV2 = false # 基本0 245 | frame.RSV3 = false # 基本0 246 | frame.OPCODE = OpCode(opcode) 247 | frame.MASK = is_masked # SERVER => CLIENTの場合はFalseでよし 248 | frame.DATA = m # 送信データ 249 | 250 | # 分割して送信される場合(初回~最後の1つ前まで) 251 | elif length > 0 and length > index and index != 0: 252 | frame.FIN = false # 一発終了 253 | frame.RSV1 = false # 基本0 254 | frame.RSV2 = false # 基本0 255 | frame.RSV3 = false # 基本0 256 | frame.OPCODE = OpCode.CONTINUATION 257 | frame.MASK = is_masked # SERVER => CLIENTの場合はFalseでよし 258 | frame.DATA = m # 送信データ 259 | 260 | elif length > 0 and length == index: 261 | frame.FIN = true # 一発終了 262 | frame.RSV1 = false # 基本0 263 | frame.RSV2 = false # 基本0 264 | frame.RSV3 = false # 基本0 265 | frame.OPCODE = OpCode.CONTINUATION 266 | frame.MASK = is_masked # SERVER => CLIENTの場合はFalseでよし 267 | frame.DATA = m # 送信データ 268 | 269 | var stream = newStringStream() 270 | 271 | # 先頭1bitから8bit 272 | var bytes_0_7: uint8 273 | # 先頭4bit 274 | if frame.FIN: 275 | bytes_0_7 = 0x80 # 1000_0000 276 | 277 | if frame.RSV1: 278 | bytes_0_7 = bytes_0_7 or 0x40 # 1100_0000 279 | 280 | if frame.RSV2: 281 | bytes_0_7 = bytes_0_7 or 0x20 # 1x10_0000 282 | 283 | if frame.RSV3: 284 | bytes_0_7 = bytes_0_7 or 0x10 # 1xx1_0000 285 | 286 | # OPCODE(下4桁) 287 | bytes_0_7 = bytes_0_7 or frame.OPCODE.uint8() 288 | stream.write(bytes_0_7) 289 | 290 | # 先頭9bitから16bit 291 | var bytes_8_15: uint8 292 | if frame.MASK: 293 | bytes_8_15 = 0x80 294 | else: 295 | bytes_8_15 = 0x00 296 | 297 | var data_length = frame.DATA.len() 298 | if data_length <= 125: 299 | bytes_8_15 = bytes_8_15 or data_length.uint8() 300 | elif data_length == 126: 301 | bytes_8_15 = bytes_8_15 or 126.uint8() 302 | else: 303 | bytes_8_15 = bytes_8_15 or 127.uint8() 304 | stream.write(bytes_8_15) 305 | 306 | if data_length == 126: 307 | # 後続2byteが16bit無符号整数に解釈されて長さになる 308 | var length_16 = data_length.uint16() 309 | # 右シフト2回で先頭から2回追加(uint8に変換すれば余計なものはそぎ落とされる) 310 | stream.write((length_16 shr 8).uint8()) # 先頭8bit 311 | stream.write(length_16.uint8()) # お尻8bit 312 | 313 | elif data_length > 0xffff: 314 | # 後続8byteが長さなのでuint64にして突っ込む 315 | var length_16_80 = data_length.uint64() 316 | # 右シフト8回で先頭から8回追加(uint8に変換すれば余計なものはそぎ落とされる) 317 | stream.write(((length_16_80 shr 56) and 0x0000_0000_0000_00ff).uint8()) 318 | stream.write(((length_16_80 shr 48) and 0x0000_0000_0000_00ff).uint8()) 319 | stream.write(((length_16_80 shr 40) and 0x0000_0000_0000_00ff).uint8()) 320 | stream.write(((length_16_80 shr 32) and 0x0000_0000_0000_00ff).uint8()) 321 | stream.write(((length_16_80 shr 24) and 0x0000_0000_0000_00ff).uint8()) 322 | stream.write(((length_16_80 shr 16) and 0x0000_0000_0000_00ff).uint8()) 323 | stream.write(((length_16_80 shr 8) and 0x0000_0000_0000_00ff).uint8()) 324 | stream.write((length_16_80 and 0x0000_0000_0000_00ff).uint8()) 325 | 326 | # マスク 327 | if frame.MASK: 328 | # サーバーからの送信は基本マスクはしない 329 | var mask_key :seq[char] = generateMaskKey() 330 | var masked_m :string 331 | 332 | # 結果のデータ[i] = 元のデータ[i] xor key [i mod 4] 333 | for index in countup(0, m.len() - 1): 334 | masked_m.add((m[index].uint8() xor mask_key[index mod 4].uint8()).char()) 335 | 336 | stream.write(mask_key[0].uint8()) 337 | stream.write(mask_key[1].uint8()) 338 | stream.write(mask_key[2].uint8()) 339 | stream.write(mask_key[3].uint8()) 340 | stream.write(masked_m) 341 | 342 | else: 343 | # ペイロード 344 | stream.write(m) 345 | 346 | # 送信 347 | stream.setPosition(0) 348 | 349 | try: 350 | await ws.socket.send(stream.readAll()) 351 | except: # その他の例外をキャッチ 352 | await ws.sendMessage("", 0x8) # Close 353 | raise newException(WebSocketOtherError, "データ送信時に未知のエラーが発生しました。") 354 | 355 | proc receiveFrame(ws: WebSocket): Future[Frame] {.async.} = 356 | ##[ 357 | 358 | ]## 359 | var frame = Frame() 360 | var receive: string 361 | receive = await ws.socket.recv(2) # 2Byte 362 | 363 | let bytes_0_7: string = convertBinaryStrings(receive[0]) # 逐次2進数に変換して判定 364 | 365 | frame.FIN = bytes_0_7[0] == '1' 366 | frame.RSV1 = bytes_0_7[1] == '1' 367 | frame.RSV2 = bytes_0_7[2] == '1' 368 | frame.RSV3 = bytes_0_7[3] == '1' 369 | 370 | case bytes_0_7[4..7] 371 | of "0000": 372 | frame.OPCODE = OpCode.CONTINUATION 373 | of "0001": 374 | frame.OPCODE = OpCode.TEXT 375 | of "0010": 376 | frame.OPCODE = OpCode.BINARY 377 | of "1000": 378 | frame.OPCODE = OpCode.CLOSE 379 | of "1001": 380 | frame.OPCODE = OpCode.PING 381 | of "1010": 382 | frame.OPCODE = OpCode.PONG 383 | else: 384 | # 未知なOpCodeが受信された場合は接続をCloseする 385 | ws.status = ConnectionStatus.CLOSING 386 | raise newException(UnknownOpCodeReceiveError, "未知なOpCodeを受信しました。($#)" % [$(bytes_0_7[4..7])]) 387 | 388 | let bytes_8_15: string = convertBinaryStrings(receive[1]) # 逐次2進数に変換して判定 389 | frame.MASK = bytes_8_15[0] == '1' 390 | # if not frame.MASK: 391 | # マスクされていないフレームを受信した際には接続をCloseする 392 | # raise newException(NoMaskedFrameReceiveError, "マスクされていないフレームを受信しました。($#)" % [$(bytes_8_15[0])]) 393 | 394 | # ペイロードの長さ 395 | frame.PAYLOAD_LENGTH = fromBin[int8]("0b" & bytes_8_15[1..7]) 396 | let payload_length = fromBin[int8]("0b" & bytes_8_15[1..7]) 397 | 398 | var true_payload_length: uint = 0 399 | # 真のペイロードの長さ 400 | if payload_length == 0x7e: 401 | # 126 402 | var length = await ws.socket.recv(2) 403 | true_payload_length = cast[ptr uint16](length[0].addr)[].htons() 404 | 405 | elif payload_length == 0x7f: 406 | # 127 407 | var length = await ws.socket.recv(8) 408 | true_payload_length = cast[ptr uint32](length[4].addr)[].htonl() 409 | 410 | else: 411 | # 0~125まで 412 | true_payload_length = uint8(payload_length) 413 | 414 | # マスクキー取得 415 | if frame.MASK: 416 | frame.MASK_KEY = await ws.socket.recv(4) 417 | 418 | # 素のデータを引っ張ってくる 419 | var data = await ws.socket.recv(int true_payload_length) 420 | 421 | # 非マスク化 422 | if frame.MASK: 423 | for index in countup(0, data.len() - 1): 424 | frame.DATA.add($(data[index].uint8() xor frame.MASK_KEY[index mod 4].uint8()).char()) 425 | else: 426 | frame.DATA.add(data) 427 | 428 | return frame 429 | 430 | proc receiveMessage*(ws: WebSocket): Future[tuple[opcode: OpCode, message: string]] {.async, gcsafe.} = 431 | ##[ 432 | 433 | ]## 434 | var receive_result: tuple[opcode: OpCode, message: string] 435 | var code: OpCode 436 | var message: string 437 | 438 | var frame = Frame() 439 | try: 440 | frame = await ws.receiveFrame() 441 | message &= frame.DATA 442 | 443 | while frame.FIN == false: 444 | frame = await ws.receiveFrame() 445 | # 結果は逐一保存 446 | code = frame.OpCode 447 | message &= frame.DATA 448 | 449 | if frame.OpCode != CONTINUATION: 450 | raise newException(UnknownOpCodeReceiveError, "継続以外のOpCodeを受信しました。($#)" % [$(frame.OpCode)]) 451 | 452 | except UnknownOpCodeReceiveError: 453 | await ws.sendMessage("", 0x8) # Close 454 | raise newException(UnknownOpCodeReceiveError, "未知のOPCODEを持つデータを受信しました。") 455 | 456 | #except NoMaskedFrameReceiveError: 457 | # await ws.sendMessage("", 0x8) # Close 458 | # raise newException(NoMaskedFrameReceiveError, "マスクされていないデータを受信しました。") 459 | 460 | except: # その他の例外をキャッチ 461 | await ws.sendMessage("", 0x8) # Close 462 | raise newException(WebSocketOtherError, "データ受信時に未知のエラーが発生しました。") 463 | 464 | # OpCodeで分岐させる 465 | if frame.OPCODE == OpCode.CLOSE: 466 | # Closeフレームを受信した端点は、それまでにCloseフレームを送信していなかったならば、 467 | # 応答として Close フレームを送信しなければならない。 468 | await ws.sendMessage("", 0x8) # Close 469 | ws.status = ConnectionStatus.CLOSED 470 | ws.socket.close() 471 | code = OpCode.CLOSE 472 | 473 | if frame.OPCODE == OpCode.PING: 474 | # PINGフレームを受信したら即座にPONGフレームを返す 475 | await ws.sendMessage("", 0xa) 476 | code = OpCode.PING 477 | 478 | if frame.OPCODE == OpCode.PONG: 479 | code = OpCode.PONG 480 | 481 | if frame.OPCODE == OpCode.TEXT: 482 | code = OpCode.TEXT 483 | 484 | if frame.OPCODE == OpCode.BINARY: 485 | # [TODO] いつか実装したいね 486 | code = OpCode.BINARY 487 | 488 | receive_result = (code, message) 489 | return receive_result --------------------------------------------------------------------------------