├── .appveyor.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── Rakefile ├── TODO ├── build_config.rb ├── build_config.rb.lock ├── example ├── server.rb └── uds.rb ├── mrbgem.rake ├── mrblib └── mrb_simplehttpserver.rb └── test └── mrb_simplehttpserver.rb /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2017 Sebastian Katzer 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | version: "{build}" 24 | 25 | environment: 26 | global: 27 | COMPILER: C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1 28 | TOOLCHAIN: gcc 29 | matrix: 30 | - MRUBY_VERSION: 1.3.0 31 | - MRUBY_VERSION: 1.4.0 32 | - MRUBY_VERSION: head 33 | 34 | matrix: 35 | allow_failures: 36 | - MRUBY_VERSION: head 37 | 38 | init: 39 | - SET PATH=%COMPILER%\bin;C:\Ruby25-x64\bin;%PATH%;C:\cygwin64\bin 40 | - gcc --version 41 | 42 | build_script: 43 | - rake mruby 44 | - IF "%MRUBY_VERSION%"=="1.3.0" (DEL /F /Q mruby\build\mrbgems\mruby-io\test\) ELSE (DEL /F /Q mruby\mrbgems\mruby-io\test\) 45 | - rake compile 46 | 47 | test_script: 48 | - rake test 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /mruby 2 | /tmp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) MATSUMOTO Ryosuke 2014 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | language: c 24 | 25 | compiler: 26 | - gcc 27 | - clang 28 | 29 | env: 30 | - MRUBY_VERSION=1.4.1 31 | - MRUBY_VERSION=head 32 | 33 | matrix: 34 | allow_failures: 35 | - env: MRUBY_VERSION=head 36 | 37 | before_script: 38 | - export TOOLCHAIN=$CC 39 | 40 | script: 41 | - rake compile 42 | - rake test 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | mruby-simplehttpserver 2 | 3 | Copyright (c) MATSUMOTO Ryosuke 2014 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | [ MIT license: http://www.opensource.org/licenses/mit-license.php ] 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mruby-simplehttpserver [![Build Status](https://travis-ci.org/matsumotory/mruby-simplehttpserver.svg?branch=master)](https://travis-ci.org/matsumotory/mruby-simplehttpserver) 2 | 3 | mruby-simplehttpserver is a HTTP Server with less dependency for mruby. mruby-simplehttpserver depends on mruby-io, mruby-socket and mruby-http. A Web server using mruby-simplehttpserver run on a environment which is not very rich like [OSv](http://osv.io/) or simple Linux box. 4 | 5 | ### Install by mrbgems 6 | 7 | add conf.gem line to `build_config.rb`: 8 | 9 | ```ruby 10 | MRuby::Build.new do |conf| 11 | 12 | # ... (snip) ... 13 | 14 | conf.gem mgem: 'mruby-simplehttpserver' 15 | end 16 | ``` 17 | 18 | ## How to use SimpleHttpServer 19 | 20 | SimpleHttpServer class provides a HTTP Server. 21 | 22 | SimpleHttpServer has a [Rack](http://rack.github.io/)-like interface, so you should provide an "app": an object that responds to `#call`, taking the environment hash as a parameter, and returning an Array with three elements: 23 | 24 | - HTTP Status Code 25 | - Headers hash 26 | - Body 27 | 28 | #### Example: a simple "OK" server 29 | 30 | The following example code can be used as the basis of a HTTP Server which returning "OK": 31 | 32 | ```ruby 33 | app = -> (env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } 34 | 35 | server = SimpleHttpServer.new( 36 | host: 'localhost', 37 | port: 8000, 38 | app: app, 39 | ) 40 | 41 | server.run 42 | ``` 43 | 44 | `SimpleHttpServer#run` invokes a server that returns "OK". (If you want to stop the server, enter `^C` key.) You can see its response with curl: 45 | 46 | ```console 47 | $ curl localhost:8000 48 | OK 49 | ``` 50 | 51 | If you see more examples, see [example/server.rb](https://github.com/matsumoto-r/mruby-simplehttpserver/blob/master/example/server.rb). 52 | 53 | #### What does `env` receive? 54 | 55 | `env`, which an "app" takes as a parameter, receives a hash object includes request headers and the following parameters: 56 | 57 | - REQUEST\_METHOD ... GET, PUT, POST, DELETE and so on. 58 | - PATH\_INFO ... request path or '/' 59 | - QUERY\_STRING ... query string 60 | - HTTP\_VERSION ... 'HTTP/1.1' 61 | 62 | If you want to see how to parse an request, see also [mattn/mruby-http](https://github.com/mattn/mruby-http). 63 | 64 | Public Class Methods 65 | --- 66 | 67 | ### new(server\_ip = nil, port = nil, nonblock = nil, app = nil, debug = nil) 68 | 69 | Creates a new SimpleHttpServer object. 70 | 71 | The `:server_ip` should be a DNS hostname or IP address, the `:port` should be the listen port that server operates on. 72 | 73 | If the `:nonblock` is `true`, take non-blocking mode. When default (nonblock = nil), it behaves blocking-mode. 74 | 75 | The `:app` should be an object that responds to `#call`, taking the environment hash as a parameter, and returning an Array with three elements: 76 | 77 | - HTTP Status Code (Integer) 78 | - Headers hash (Hash) 79 | - Body (Array) 80 | 81 | it's like a [Rack](http://rack.github.io/) interface. 82 | 83 | If you want to debug, you can set `:debug` true. 84 | 85 | Public Instance Methods 86 | --- 87 | 88 | ### run() 89 | 90 | A process requests on sock. 91 | 92 | ## License 93 | 94 | under the MIT License: 95 | 96 | - see [LICENSE](./LICENSE) file 97 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) Sebastian Katzer 2017 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | ENV['MRUBY_CONFIG'] ||= File.expand_path('build_config.rb') 24 | ENV['MRUBY_VERSION'] ||= '2.1.2' 25 | 26 | file :mruby do 27 | if ENV['MRUBY_VERSION'] == 'head' 28 | sh 'git clone --depth 1 git://github.com/mruby/mruby.git' 29 | else 30 | sh "curl -L --fail --retry 3 --retry-delay 1 https://github.com/mruby/mruby/archive/#{ENV['MRUBY_VERSION']}.tar.gz -s -o - | tar zxf -" # rubocop:disable LineLength 31 | mv "mruby-#{ENV['MRUBY_VERSION']}", 'mruby' 32 | end 33 | end 34 | 35 | Rake::Task[:mruby].invoke 36 | 37 | namespace :mruby do 38 | Dir.chdir('mruby') { load 'Rakefile' } 39 | end 40 | 41 | task default: :compile 42 | 43 | desc 'compile binary' 44 | task compile: 'mruby:all' 45 | 46 | desc 'test' 47 | task test: 'mruby:test' 48 | 49 | desc 'cleanup' 50 | task clean: 'mruby:clean' 51 | 52 | desc 'cleanup all' 53 | task cleanall: 'mruby:deep_clean' 54 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | More example 2 | Test 3 | 4 | -------------------------------------------------------------------------------- /build_config.rb: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) MATSUMOTO Ryosuke 2014 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | MRuby::Build.new do |conf| 24 | toolchain ENV.fetch('TOOLCHAIN', :gcc) 25 | 26 | conf.enable_debug 27 | conf.enable_test 28 | 29 | conf.gem __dir__ 30 | conf.gembox :default 31 | end 32 | -------------------------------------------------------------------------------- /build_config.rb.lock: -------------------------------------------------------------------------------- 1 | --- 2 | mruby_version: 3 | version: 2.1.2 4 | release_no: 20102 5 | builds: 6 | host: 7 | https://github.com/mattn/mruby-http.git: 8 | url: https://github.com/mattn/mruby-http.git 9 | branch: HEAD 10 | commit: 003c2af0a059c50a3546001860108ff4bf3a2cac 11 | version: 0.0.0 12 | https://github.com/katzer/mruby-shelf.git: 13 | url: https://github.com/katzer/mruby-shelf.git 14 | branch: HEAD 15 | commit: 3bf2bc69eaceb4555ded37081e61c247808e6d88 16 | version: 0.0.0 17 | https://github.com/ksss/mruby-stringio.git: 18 | url: https://github.com/ksss/mruby-stringio.git 19 | branch: HEAD 20 | commit: 97cc4434519e85a0785a1b4cf6f872a220f772db 21 | version: 0.0.0 22 | https://github.com/katzer/mruby-process.git: 23 | url: https://github.com/katzer/mruby-process.git 24 | branch: HEAD 25 | commit: 03a4f8110b4d32b5ff56f78560c255e80dcf9219 26 | version: 0.0.0 27 | https://github.com/katzer/mruby-r3.git: 28 | url: https://github.com/katzer/mruby-r3.git 29 | branch: HEAD 30 | commit: c5b5ae647a8f04a86b4cd519273ba243bd459aec 31 | version: 0.0.0 32 | https://github.com/iij/mruby-env.git: 33 | url: https://github.com/iij/mruby-env.git 34 | branch: HEAD 35 | commit: 056ae324451ef16a50c7887e117f0ea30921b71b 36 | version: 0.0.0 37 | https://github.com/katzer/mruby-os.git: 38 | url: https://github.com/katzer/mruby-os.git 39 | branch: HEAD 40 | commit: 8e6e1dca8284f142bea681bfe032ebe7bae3851a 41 | version: 0.0.0 42 | -------------------------------------------------------------------------------- /example/server.rb: -------------------------------------------------------------------------------- 1 | app = Proc.new do |env| 2 | code = 200 3 | headers = { 'Server' => 'mruby-simplehttpserver' } 4 | path = env['PATH_INFO'] 5 | method = env['REQUEST_METHOD'] 6 | body = nil 7 | 8 | case path 9 | when '/mruby' 10 | body = "Hello mruby World.\n" 11 | when "/html" 12 | headers['Content-type'] = 'text/html; charset=utf-8' 13 | body = "

Hello mruby World.

\n" 14 | when "/notfound" 15 | # Custom error response message 16 | body = "Not Found on this server: #{path}\n" 17 | code = 404 18 | end 19 | 20 | [code, headers, [body]] 21 | end 22 | 23 | # 24 | # Server Configration 25 | # 26 | server = SimpleHttpServer.new( 27 | server_ip: 'localhost', 28 | port: 8000, 29 | debug: true, 30 | app: app, 31 | ) 32 | 33 | server.run 34 | -------------------------------------------------------------------------------- /example/uds.rb: -------------------------------------------------------------------------------- 1 | class App 2 | def self.call(env) 3 | code = 200 4 | headers = { 'Server' => 'mruby-simplehttpserver' } 5 | path = env['PATH_INFO'] 6 | method = env['REQUEST_METHOD'] 7 | body = nil 8 | 9 | puts "Request: #{method} #{path}" 10 | 11 | case path 12 | when '/' 13 | body = "Hello mruby World.\n" 14 | when '/raise' 15 | # Custom error response message 16 | body = "This is error: #{path}\n" 17 | code = 503 18 | else 19 | body = "Not Found on this server: #{path}\n" 20 | code = 404 21 | end 22 | 23 | [code, headers, [body]] 24 | end 25 | end 26 | 27 | SimpleHttpServer.new( 28 | path: '/tmp/myserver.sock', 29 | debug: true, 30 | app: App, 31 | ).tap{|srv| puts("Server is running: #{srv}") }.run 32 | -------------------------------------------------------------------------------- /mrbgem.rake: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) MATSUMOTO Ryosuke 2014 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | require "#{MRUBY_ROOT}/lib/mruby/source" 24 | 25 | MRuby::Gem::Specification.new('mruby-simplehttpserver') do |spec| 26 | spec.license = 'MIT' 27 | spec.authors = ['MATSUMOTO Ryosuke', 'KATZER Sebastian'] 28 | spec.version = '1.0.0' 29 | 30 | spec.add_dependency('mruby-time', core: 'mruby-time') 31 | spec.add_dependency('mruby-http') 32 | spec.add_dependency('mruby-shelf') 33 | spec.add_dependency('mruby-stringio') 34 | 35 | if MRuby::Source::MRUBY_VERSION >= '1.4.0' 36 | spec.add_dependency('mruby-io', core: 'mruby-io') 37 | spec.add_dependency('mruby-socket', core: 'mruby-socket') 38 | else 39 | spec.add_dependency('mruby-io') 40 | spec.add_dependency('mruby-socket') 41 | end 42 | 43 | spec.add_test_dependency('mruby-process', mgem: 'mruby-process2') 44 | end 45 | -------------------------------------------------------------------------------- /mrblib/mrb_simplehttpserver.rb: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) MATSUMOTO Ryosuke 2014 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Represents a TCP/IP server socket who delegates the requests to a shelf app. 24 | class SimpleHttpServer 25 | SEP = "\r\n".freeze 26 | RECV_BUF = 1024 27 | HTTP_VERSION = 'HTTP/1.1'.freeze 28 | ROOT_PATH = '/'.freeze 29 | 30 | # Initializes the server via a config hash map. 31 | # 32 | # @param [ Hash ] config Required are :host, :port and :app 33 | # 34 | # @return [ Void ] 35 | def initialize(config) 36 | @config = config 37 | @host = config[:host] || config[:server_ip] 38 | @port = config[:port] 39 | @path = config[:path] 40 | @nonblock = config[:nonblock] != false 41 | @timeout = [0, config[:timeout] || 5].max 42 | @app = config[:app] 43 | @parser = HTTP::Parser.new 44 | end 45 | 46 | attr_reader :config, :host, :port, :path 47 | 48 | # Bind to the host and port and wait for incomming connections. 49 | # 50 | # @return [ Void ] 51 | def run 52 | srv = if self.path 53 | UNIXServer.new(path) 54 | else 55 | TCPServer.new(host, port) 56 | end 57 | 58 | loop do 59 | io = accept_connection(srv) 60 | 61 | begin 62 | data = receive_data(io) 63 | send_data(io, handle_data(io, data)) if data 64 | rescue 65 | raise 'Connection reset by peer' if config[:debug] && io.closed? 66 | ensure 67 | io.close rescue nil 68 | GC.start if config[:run_gc_per_request] 69 | end 70 | end 71 | end 72 | 73 | private 74 | 75 | # Wait for incoming socket connection. 76 | # 77 | # @param [ TCPServer ] tcp The TCP server which is bind to a port. 78 | # 79 | # @return [ BasicSocket ] 80 | def accept_connection(tcp) 81 | counter = counter ? counter + 1 : 1 82 | 83 | sock = BasicSocket.for_fd(tcp.sysaccept) 84 | sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_NOSIGPIPE, true) if Socket.const_defined? :SO_NOSIGPIPE 85 | sock 86 | rescue RuntimeError => e 87 | counter == 1 ? retry : raise(e) 88 | end 89 | 90 | # Receive data from the socket in a loop until all data have been received. 91 | # Might break out of the loop in case of a timeout. 92 | # 93 | # @param [ BasicSocket ] io The tcp socket from where to read the data. 94 | # 95 | # @return [ String ] nil if no data could be read. 96 | def receive_data(io) 97 | data = nil 98 | time = Time.now if @nonblock 99 | ext = String.method_defined? :<< 100 | 101 | loop do 102 | begin 103 | buf = io.recv(RECV_BUF, @nonblock ? Socket::MSG_DONTWAIT : 0) 104 | 105 | if !data 106 | data = buf 107 | elsif ext 108 | data << buf 109 | else 110 | data += buf 111 | end 112 | 113 | return data if buf.size != RECV_BUF 114 | rescue 115 | next if (Time.now - time) < @timeout 116 | end 117 | end 118 | end 119 | 120 | # Parse the HTTP request to pass the env to the shelf app 121 | # and return the HTTP response string. 122 | # 123 | # @param [ BasicSocket ] io The tcp socket from where to read the data. 124 | # @param [ String ] data The data reveiced from the socket. 125 | # 126 | # @return [ String ] 127 | def handle_data(io, data) 128 | request = @parser.parse_request(data) 129 | env = request_to_env(request) 130 | status, header, body = @app.call(env) 131 | 132 | create_response(status, header, body.join) 133 | end 134 | 135 | # Send data back to the client. 136 | # 137 | # @param [ BasicSocket ] io The tcp socket where to send the data. 138 | # @param [ String ] data The data to send. 139 | # 140 | # @return [ String ] 141 | def send_data(io, data) 142 | loop do 143 | n = io.syswrite(data) 144 | return if n == data.bytesize 145 | data = data[n..-1] 146 | end 147 | end 148 | 149 | # Convert the parsed HTTP request into an environemt hash 150 | # to be passed to the shelf app. 151 | # 152 | # @param [ HTTP::Request ] req The parsed HTTP request object. 153 | # 154 | # @return [ Hash ] 155 | def request_to_env(req) 156 | string_io = StringIO.new 157 | string_io.write req.body 158 | string_io.rewind 159 | 160 | req.headers.merge( 161 | Shelf::REQUEST_METHOD => req.method, 162 | Shelf::PATH_INFO => req.path || ROOT_PATH, 163 | Shelf::QUERY_STRING => req.query, 164 | Shelf::HTTP_VERSION => HTTP_VERSION, 165 | Shelf::SERVER_NAME => 'mruby-simplehttpserver', 166 | Shelf::SERVER_ADDR => host, 167 | Shelf::SERVER_PORT => port, 168 | Shelf::SHELF_URL_SCHEME => req.schema, 169 | Shelf::SHELF_INPUT => string_io 170 | ) 171 | end 172 | 173 | # Convert the response returned by @app.call into a HTTP response. 174 | # https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html 175 | # 176 | # @param [ Fixnum ] code The HTTP status code. 177 | # @param [ Hash ] header The header as a key-value list. 178 | # @param [ String ] body The message body. 179 | # 180 | # @return [ String ] 181 | def create_response(code, header, body) 182 | header[:Date] ||= http_date 183 | header[:Connection] = :close 184 | 185 | header_ary = [] 186 | header.each { |k, v| header_ary << ["#{k}:#{v}"] if v } 187 | 188 | http_status_line(code) + SEP \ 189 | + header_ary.join(SEP) + SEP + SEP \ 190 | + body 191 | end 192 | 193 | # Return the HTTP status line including the HTTP version, response code 194 | # and short description. 195 | # 196 | # @param [ Fixnum ] code The HTTP status code. 197 | # Defaults to: 200 (OK) 198 | # 199 | # @return [ String ] 200 | def http_status_line(code = 200) 201 | "#{HTTP_VERSION} #{code} #{Shelf::Utils::HTTP_STATUS_CODES[code]}" 202 | end 203 | 204 | # The date and time at which the message was originated. 205 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date 206 | # 207 | # @return [ String ] 208 | def http_date 209 | tp = Time.now.gmtime.to_s.split 210 | "#{tp[0]}, #{tp[2]} #{tp[1]} #{tp[5]} #{tp[3]} GMT" 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /test/mrb_simplehttpserver.rb: -------------------------------------------------------------------------------- 1 | ## 2 | ## SimpleHttpServer Test 3 | ## 4 | 5 | ## Setup configuration 6 | host = 'localhost' 7 | port = 8000 8 | app = Proc.new do |env| 9 | code = 200 10 | headers = { 'Server' => 'mruby-simplehttpserver' } 11 | path = env['PATH_INFO'] 12 | method = env['REQUEST_METHOD'] 13 | body = nil 14 | 15 | case path 16 | when '/mruby' 17 | body = "Hello mruby World.\n" 18 | when '/html' 19 | headers['Content-type'] = 'text/html; charset=utf-8' 20 | body = '

Hello mruby World.

' 21 | when '/echo' 22 | body = env['shelf.input'].read 23 | when '/notfound' 24 | # Custom error response message 25 | body = "Not Found on this server: #{path}" 26 | code = 404 27 | end 28 | 29 | [code, headers, [body]] 30 | end 31 | 32 | assert 'SimpleHttpServer#initialize' do 33 | server = SimpleHttpServer.new(server_ip: host, port: port, app: app) 34 | 35 | assert_kind_of Hash, server.config 36 | assert_equal host, server.host 37 | assert_equal port, server.port 38 | end 39 | 40 | assert 'SimpleHttpServer#config' do 41 | server = SimpleHttpServer.new(server_ip: host, port: port, app: app) 42 | 43 | assert_include server.config, :server_ip 44 | assert_kind_of String, server.config[:server_ip] 45 | assert_equal host, server.config[:server_ip] 46 | 47 | assert_include server.config, :port 48 | assert_kind_of Integer, server.config[:port] 49 | assert_equal port, server.config[:port] 50 | 51 | assert_include server.config, :app 52 | assert_equal app, server.config[:app] 53 | 54 | assert_not_include server.config, :nonblock 55 | assert_nil server.config[:nonblock] 56 | 57 | # Update configuration 58 | server.config[:nonblock] = true 59 | 60 | assert_include server.config, :nonblock 61 | assert_true server.config[:nonblock] 62 | end 63 | 64 | assert 'SimpleHttpServer#run' do 65 | begin 66 | server = SimpleHttpServer.new(server_ip: host, port: port, app: app) 67 | pid = fork { server.run } 68 | 69 | h = HTTP::Parser.new 70 | 71 | res = `curl -si localhost:8000/mruby` 72 | h.parse_response(res) {|x| 73 | assert_equal 'mruby-simplehttpserver', x.headers['Server'] 74 | assert_nil x.headers['Content-type'] 75 | assert_equal "Hello mruby World.\n", x.body 76 | } 77 | 78 | res = `curl -si localhost:8000/html` 79 | h.parse_response(res) {|x| 80 | assert_equal 'text/html; charset=utf-8', x.headers['Content-type'] 81 | assert_equal "

Hello mruby World.

", x.body 82 | } 83 | 84 | res = `curl -si localhost:8000/echo -d 'This is body'` 85 | h.parse_response(res) {|x| 86 | assert_equal "This is body", x.body 87 | } 88 | 89 | res = `curl -si localhost:8000/notfound` 90 | h.parse_response(res) {|x| 91 | assert_equal 'Not Found on this server: /notfound', x.body 92 | } 93 | 94 | Process.kill :TERM, pid 95 | rescue NoMethodError => e 96 | skip e.message 97 | end 98 | end 99 | --------------------------------------------------------------------------------