├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── config.v ├── constant.v ├── cookie.v ├── examples ├── basic.v ├── json.v ├── send_file.v └── static_folder │ ├── index.html │ ├── main.v │ └── static │ ├── script.js │ └── styles.css ├── route.v ├── spaceship.v └── v.mod /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.v] 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.v linguist-language=V text=auto eol=lf 2 | *.vv linguist-language=V text=auto eol=lf 3 | *.vsh linguist-language=V text=auto eol=lf 4 | **/v.mod linguist-language=V text=auto eol=lf 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | spaceship 4 | *.exe 5 | *.exe~ 6 | *.so 7 | *.dylib 8 | *.dll 9 | vls.log 10 | .vscode 11 | notes.txt 12 | tools 13 | .DS_Store 14 | *_test.v -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 cookie bacon 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spaceship 2 | 3 | ## Installation 4 | 5 | ```bash 6 | v up 7 | v install cookieforpres.spaceship 8 | ``` 9 | 10 | ## Example 11 | 12 | ```v 13 | module main 14 | 15 | import cookieforpres.spaceship 16 | 17 | fn handler(mut req spaceship.Request, mut res spaceship.Response) { 18 | res.set_body('Welcome to Spaceship 🚀. Get ready for blast off!') 19 | } 20 | 21 | fn main() { 22 | mut sp := spaceship.new('0.0.0.0', 8080) 23 | 24 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler) 25 | sp.add_route(route) 26 | 27 | sp.listen() or { panic(err) } 28 | } 29 | ``` 30 | 31 | ## Upcoming Features / Already Implemented 32 | 33 | * [X] sending files (e.g. images, html file, json file, etc.) 34 | * [X] having a static folder for css and js files 35 | * [ ] middleware 36 | 37 | if you have any suggestions or want to contribute, please feel free to open an issue or make a pull request on [GitHub](https://github.com/cookieforpres/spaceship) 38 | -------------------------------------------------------------------------------- /config.v: -------------------------------------------------------------------------------- 1 | module spaceship 2 | 3 | pub struct SpaceshipConfig { 4 | pub mut: 5 | verbose bool 6 | show_favicon_request bool 7 | show_server_header bool 8 | } 9 | 10 | pub fn (mut sp Spaceship) edit_config(id string, toggle bool) { 11 | match id { 12 | 'verbose' { sp.config.verbose = toggle } 13 | 'show_favicon_request' { sp.config.show_favicon_request = toggle } 14 | 'show_server_header' { sp.config.show_server_header = toggle } 15 | else {} 16 | } 17 | } -------------------------------------------------------------------------------- /constant.v: -------------------------------------------------------------------------------- 1 | module spaceship 2 | 3 | pub fn get_status_codes() map[int]string { 4 | mut codes := map[int]string{} 5 | codes[100] = "Continue" 6 | codes[101] = "Switching Protocols" 7 | codes[102] = "Processing" 8 | codes[200] = "OK" 9 | codes[201] = "Created" 10 | codes[202] = "Accepted" 11 | codes[203] = "Non-Authoritative Information" 12 | codes[204] = "No Content" 13 | codes[205] = "Reset Content" 14 | codes[206] = "Partial Content" 15 | codes[207] = "Multi-Status" 16 | codes[300] = "Multiple Choices" 17 | codes[301] = "Moved Permanently" 18 | codes[302] = "Found" 19 | codes[303] = "See Other" 20 | codes[304] = "Not Modified" 21 | codes[305] = "Use Proxy" 22 | codes[307] = "Temporary Redirect" 23 | codes[400] = "Bad Request" 24 | codes[401] = "Unauthorized" 25 | codes[402] = "Payment Required" 26 | codes[403] = "Forbidden" 27 | codes[404] = "Not Found" 28 | codes[405] = "Method Not Allowed" 29 | codes[406] = "Not Acceptable" 30 | codes[407] = "Proxy Authentication Required" 31 | codes[408] = "Request Timeout" 32 | codes[409] = "Conflict" 33 | codes[410] = "Gone" 34 | codes[411] = "Length Required" 35 | codes[412] = "Precondition Failed" 36 | codes[413] = "Request Entity Too Large" 37 | codes[414] = "Request-URI Too Long" 38 | codes[415] = "Unsupported Media Type" 39 | codes[416] = "Request Range Not Satisfiable" 40 | codes[417] = "Expectation Failed" 41 | codes[418] = "I'm a teapot" 42 | codes[422] = "Unprocessable Entity" 43 | codes[423] = "Locked" 44 | codes[424] = "Failed Dependency" 45 | codes[425] = "Unordered Collection" 46 | codes[426] = "Upgrade Required" 47 | codes[449] = "Retry With" 48 | codes[500] = "Internal Server Error" 49 | codes[501] = "Not Implemented" 50 | codes[502] = "Bad Gateway" 51 | codes[503] = "Service Unavailable" 52 | codes[504] = "Gateway Timeout" 53 | codes[505] = "HTTP Version Not Supported" 54 | codes[506] = "Variant Also Negotiates" 55 | codes[507] = "Insufficient Storage" 56 | codes[509] = "Bandwidth Limit Exceeded" 57 | codes[510] = "Not Extended" 58 | return codes 59 | } -------------------------------------------------------------------------------- /cookie.v: -------------------------------------------------------------------------------- 1 | module spaceship 2 | 3 | pub struct Cookie { 4 | pub mut: 5 | name string 6 | value string 7 | path string 8 | domain string 9 | expires string 10 | http_only bool 11 | secure bool 12 | max_age int 13 | same_site string 14 | } 15 | 16 | pub fn new_cookie() Cookie { 17 | return Cookie{} 18 | } 19 | 20 | pub fn (mut resp Response) add_cookie(cookie Cookie) { 21 | resp.cookies << &cookie 22 | } 23 | 24 | fn (mut resp Response) format_cookie(cookie Cookie) string { 25 | mut coo := '$cookie.name=$cookie.value; ' 26 | if cookie.path != '' { 27 | coo += 'Path=$cookie.path; ' 28 | } 29 | 30 | if cookie.domain != '' { 31 | coo += 'Domain=$cookie.domain; ' 32 | } 33 | 34 | if cookie.expires != '' { 35 | coo += 'Expires=$cookie.expires; ' 36 | } 37 | 38 | if cookie.http_only { 39 | coo += 'HttpOnly; ' 40 | } 41 | 42 | if cookie.secure { 43 | coo += 'Secure; ' 44 | } 45 | 46 | if cookie.max_age != 0 { 47 | coo += 'Max-Age=$cookie.max_age; ' 48 | } 49 | 50 | if cookie.same_site != '' { 51 | coo += 'SameSite=$cookie.same_site; ' 52 | } 53 | 54 | return coo 55 | } -------------------------------------------------------------------------------- /examples/basic.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import cookieforpres.spaceship 4 | 5 | fn handler(mut req spaceship.Request, mut res spaceship.Response) { 6 | res.set_body('Welcome to Spaceship 🚀. Get ready for blast off!') 7 | } 8 | 9 | fn main() { 10 | mut sp := spaceship.new('0.0.0.0', 8080) 11 | 12 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler) 13 | sp.add_route(route) 14 | 15 | sp.listen() or { panic(err) } 16 | } -------------------------------------------------------------------------------- /examples/json.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import cookieforpres.spaceship 4 | import json 5 | 6 | fn handler(mut req spaceship.Request, mut res spaceship.Response) { 7 | res.add_header('Content-Type', 'application/json') 8 | 9 | json_data := json.encode({'message': 'Welcome to Spaceship 🚀. Get ready for blast off!'}) 10 | res.set_body(json_data) 11 | } 12 | 13 | fn main() { 14 | mut sp := spaceship.new('0.0.0.0', 8080) 15 | 16 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler) 17 | sp.add_route(route) 18 | 19 | sp.listen() or { panic(err) } 20 | } -------------------------------------------------------------------------------- /examples/send_file.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import cookieforpres.spaceship 4 | 5 | fn handler(mut req spaceship.Request, mut res spaceship.Response) { 6 | res.add_header('Content-Type', 'application/json') 7 | res.send_file('index.json') 8 | } 9 | 10 | fn main() { 11 | mut sp := spaceship.new('0.0.0.0', 8080) 12 | 13 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler) 14 | sp.add_route(route) 15 | 16 | sp.listen() or { panic(err) } 17 | } -------------------------------------------------------------------------------- /examples/static_folder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Spaceship 🚀 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/static_folder/main.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import cookieforpres.spaceship 4 | 5 | fn handler(mut req spaceship.Request, mut res spaceship.Response) { 6 | res.add_header('Content-Type', 'text/html') 7 | res.send_file('index.html') 8 | } 9 | 10 | fn main() { 11 | mut sp := spaceship.new('0.0.0.0', 8080) 12 | sp.static_folder('static/') ? 13 | 14 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler) 15 | sp.add_route(route) 16 | 17 | sp.listen() or { panic(err) } 18 | } -------------------------------------------------------------------------------- /examples/static_folder/static/script.js: -------------------------------------------------------------------------------- 1 | function helloWorld() { 2 | console.log("hello world!") 3 | } -------------------------------------------------------------------------------- /examples/static_folder/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #fff; 3 | background-color: #333; 4 | } -------------------------------------------------------------------------------- /route.v: -------------------------------------------------------------------------------- 1 | module spaceship 2 | 3 | import os 4 | 5 | pub struct Response { 6 | mut: 7 | status_code_name string 8 | 9 | pub mut: 10 | status_code int 11 | headers map[string]string 12 | cookies []&Cookie 13 | body string 14 | } 15 | 16 | pub struct Route { 17 | pub mut: 18 | path string 19 | methods []string 20 | handler fn(mut req Request, mut res Response) 21 | } 22 | 23 | pub fn new_response() Response { 24 | mut res := Response{} 25 | res.add_header('Content-Type', 'text/plain;charset=UTF-8') 26 | res.add_header('Server', 'spaceship') 27 | 28 | return res 29 | } 30 | 31 | pub fn (mut sp Spaceship) add_route(route Route) { 32 | sp.routes << &route 33 | } 34 | 35 | pub fn (mut resp Response) set_status_code(status_code int) { 36 | codes := get_status_codes() 37 | for key, value in codes { 38 | if key == status_code { 39 | resp.status_code_name = value 40 | } 41 | } 42 | 43 | resp.status_code = status_code 44 | } 45 | 46 | pub fn (mut resp Response) add_header(key string, value string) { 47 | if key == 'Content-Type' { 48 | if value.contains('charset=') { 49 | resp.headers[key] = value 50 | } else { 51 | resp.headers[key] = value + ';charset=UTF-8' 52 | } 53 | } else { 54 | resp.headers[key] = value 55 | } 56 | } 57 | 58 | pub fn (mut resp Response) remove_header(key string) { 59 | resp.headers.delete(key) 60 | } 61 | 62 | pub fn (mut resp Response) set_body(body string) { 63 | resp.body = body 64 | } 65 | 66 | pub fn (mut resp Response) send_file(path string) { 67 | data := os.read_file(path) or { 68 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 69 | exit(1) 70 | } 71 | 72 | resp.set_body(data) 73 | } 74 | 75 | pub fn new_route(path string, methods []string, handler fn(mut req Request, mut res Response)) Route { 76 | if methods.len == 0 { 77 | return Route { 78 | path: path, 79 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH'], 80 | handler: handler 81 | } 82 | } else { 83 | return Route { 84 | path: path, 85 | methods: methods, 86 | handler: handler 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /spaceship.v: -------------------------------------------------------------------------------- 1 | module spaceship 2 | 3 | import net 4 | import time 5 | import os 6 | 7 | pub struct DefaultResponse { 8 | pub mut: 9 | status_code int 10 | status_code_name string 11 | message string 12 | } 13 | 14 | pub struct Spaceship { 15 | pub mut: 16 | host string 17 | port int 18 | 19 | routes []&Route 20 | default_responses []&DefaultResponse 21 | 22 | static_files []&StaticFile 23 | static_path string 24 | 25 | config SpaceshipConfig 26 | } 27 | 28 | pub struct Request { 29 | pub mut: 30 | method string 31 | path string 32 | body string 33 | headers map[string]string 34 | start_time time.Time 35 | conn net.TcpConn 36 | } 37 | 38 | pub enum StaticFileType { 39 | css 40 | js 41 | image 42 | other 43 | } 44 | 45 | pub struct StaticFile { 46 | pub mut: 47 | file_path string 48 | file_type StaticFileType 49 | } 50 | 51 | pub enum ErrorMessage { 52 | no_error 53 | not_found 54 | method_not_allowed 55 | internal_server_error 56 | } 57 | 58 | pub fn (mut sp Spaceship) static_folder(path string) ? { 59 | mut path_ := path 60 | for { 61 | if !path_.ends_with('/') { 62 | break 63 | } 64 | 65 | path_ = path_.trim_right('/') 66 | } 67 | 68 | if !os.is_dir(path_) { 69 | eprintln('[\x1b[31;1merror\x1b[0m] $path_ is not a directory or does not exist') 70 | exit(1) 71 | } 72 | 73 | mut files := []&StaticFile{} 74 | mut pfiles := &files 75 | 76 | os.walk(path_, fn [mut pfiles] (f string) { 77 | if f.ends_with('.css') { 78 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.css} 79 | pfiles << file 80 | } else if f.ends_with('.js') { 81 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.js} 82 | pfiles << file 83 | } else if f.ends_with('.png') { 84 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.image} 85 | pfiles << file 86 | } else { 87 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.other} 88 | pfiles << file 89 | } 90 | }) 91 | 92 | sp.static_files = files 93 | sp.static_path = path_ 94 | } 95 | 96 | fn get_path(str string) string { 97 | return str.split(' ')[1] 98 | } 99 | 100 | fn get_method(str string) string { 101 | return str.split(' ')[0] 102 | } 103 | 104 | fn get_body(str string) string { 105 | parts := str.split('\r\n\r\n') 106 | if parts.len > 1 { 107 | return parts[1] 108 | } else { 109 | return '' 110 | } 111 | } 112 | 113 | fn get_headers(strs []string) map[string]string { 114 | mut headers := map[string]string{} 115 | for str in strs { 116 | parts := str.split(':') 117 | if parts.len == 2 { 118 | headers[parts[0]] = parts[1] 119 | } 120 | } 121 | return headers 122 | } 123 | 124 | fn log_request(mut sp Spaceship, host string, status_code int, method string, path string, response_time string) { 125 | if !sp.config.show_favicon_request && path.contains('favicon.ico') { 126 | return 127 | } else { 128 | if status_code >= 200 && status_code < 300 { 129 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[32;1m$status_code\x1b[0m $path - $response_time') 130 | } else if status_code >= 300 && status_code < 400 { 131 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[33;1m$status_code\x1b[0m $path - $response_time') 132 | } else if status_code >= 400 && status_code < 500 { 133 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[33;1m$status_code\x1b[0m $path - $response_time') 134 | } else { 135 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[31;1m$status_code\x1b[0m $path - $response_time') 136 | } 137 | } 138 | } 139 | 140 | fn run_response(mut sp Spaceship, mut conn net.TcpConn, mut request Request, mut response Response) ? { 141 | if response.status_code == 0 { 142 | response.set_status_code(200) 143 | } 144 | 145 | mut resp := 'HTTP/1.1 $response.status_code $response.status_code_name\r\n' 146 | for key, value in response.headers { 147 | resp += '$key: $value\r\n' 148 | } 149 | 150 | for cookie in response.cookies { 151 | formated_cookie := response.format_cookie(cookie) 152 | resp += 'Set-Cookie: $formated_cookie\r\n' 153 | } 154 | 155 | resp += '\r\n' 156 | resp += response.body 157 | 158 | 159 | conn.write_string(resp) ? 160 | 161 | diff := time.since(request.start_time) 162 | 163 | if sp.config.verbose { 164 | log_request(mut sp, conn.peer_ip() ?, response.status_code, request.method, request.path, '$diff') 165 | } 166 | } 167 | 168 | fn (mut sp Spaceship) handle_connection(mut conn net.TcpConn) ? { 169 | mut message := '' 170 | for { 171 | mut line := conn.read_line() 172 | bytes := line.bytes() 173 | 174 | if bytes.len < 2 { 175 | break 176 | } 177 | 178 | if bytes[0] == `\r` && bytes[1] == `\n` { 179 | break 180 | } 181 | 182 | message += line 183 | } 184 | 185 | start := time.now() 186 | 187 | if message.len == 0 { 188 | conn.close() ? 189 | return 190 | } 191 | 192 | message_parts := message.split('\r\n') 193 | 194 | method := get_method(message_parts[0]) 195 | path := get_path(message_parts[0]) 196 | headers := get_headers(message_parts[1..]) 197 | body := get_body(message) 198 | 199 | mut request := Request{method: method, path: path, body: body, headers: headers, start_time: start, conn: conn} 200 | mut response := new_response() 201 | 202 | mut error_message := ErrorMessage.no_error 203 | mut found := false 204 | for route in sp.routes { 205 | if route.path == request.path { 206 | if method in route.methods { 207 | route.handler(mut request, mut response) 208 | found = true 209 | break 210 | } else if route.methods.len == 0 { 211 | route.handler(mut request, mut response) 212 | found = true 213 | break 214 | } else { 215 | error_message = ErrorMessage.method_not_allowed 216 | break 217 | } 218 | } 219 | 220 | if sp.static_path != '' { 221 | sp.static_folder(sp.static_path) ? 222 | } 223 | 224 | for file in sp.static_files { 225 | static_path := file.file_path.substr(sp.static_path.len, file.file_path.len) 226 | 227 | if path == static_path { 228 | match file.file_type { 229 | .css { 230 | response.set_status_code(200) 231 | response.add_header('Content-Type', 'text/css') 232 | 233 | contents := os.read_file(file.file_path) or { 234 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 235 | exit(1) 236 | } 237 | 238 | response.set_body(contents) 239 | found = true 240 | break 241 | } 242 | .js { 243 | response.set_status_code(200) 244 | response.add_header('Content-Type', 'text/javascript') 245 | 246 | contents := os.read_file(file.file_path) or { 247 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 248 | exit(1) 249 | } 250 | 251 | response.set_body(contents) 252 | found = true 253 | break 254 | } 255 | .image { 256 | response.set_status_code(200) 257 | response.add_header('Content-Type', 'image/png') 258 | 259 | contents := os.read_file(file.file_path) or { 260 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 261 | exit(1) 262 | } 263 | 264 | response.set_body(contents) 265 | found = true 266 | break 267 | } 268 | .other { 269 | response.set_status_code(200) 270 | response.add_header('Content-Type', 'text/plain') 271 | 272 | contents := os.read_file(file.file_path) or { 273 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 274 | exit(1) 275 | } 276 | 277 | response.set_body(contents) 278 | found = true 279 | break 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | if !found && error_message != ErrorMessage.method_not_allowed { 287 | error_message = ErrorMessage.not_found 288 | } 289 | 290 | if !sp.config.show_server_header { 291 | response.remove_header('Server') 292 | } 293 | 294 | match error_message { 295 | .method_not_allowed { 296 | for dr in sp.default_responses { 297 | if dr.status_code == 405 { 298 | response.set_status_code(dr.status_code) 299 | response.set_body(dr.message) 300 | break 301 | } 302 | } 303 | 304 | run_response(mut sp, mut conn, mut request, mut response) ? 305 | conn.close() ? 306 | } 307 | .not_found { 308 | for dr in sp.default_responses { 309 | if dr.status_code == 404 { 310 | response.set_status_code(dr.status_code) 311 | response.set_body(dr.message) 312 | break 313 | } 314 | } 315 | 316 | run_response(mut sp, mut conn, mut request, mut response) ? 317 | conn.close() ? 318 | } 319 | else { 320 | run_response(mut sp, mut conn, mut request, mut response) ? 321 | conn.close() ? 322 | } 323 | } 324 | } 325 | 326 | pub fn new(host string, port int) Spaceship { 327 | mut default_responses := []&DefaultResponse{} 328 | default_responses << &DefaultResponse{ 329 | status_code: 404, 330 | status_code_name: 'Not Found', 331 | message: 'Not Found', 332 | } 333 | 334 | default_responses << &DefaultResponse{ 335 | status_code: 500, 336 | status_code_name: 'Internal Server Error', 337 | message: 'Internal Server Error', 338 | } 339 | 340 | default_responses << &DefaultResponse{ 341 | status_code: 405, 342 | status_code_name: 'Method Not Allowed', 343 | message: 'Method Not Allowed', 344 | } 345 | 346 | return Spaceship{ 347 | host: host, 348 | port: port, 349 | default_responses: default_responses, 350 | static_path: '', 351 | config: SpaceshipConfig { 352 | verbose: true 353 | show_favicon_request: true 354 | show_server_header: true 355 | } 356 | } 357 | } 358 | 359 | pub fn (mut sp Spaceship) listen() ? { 360 | mut ln := net.listen_tcp(net.AddrFamily.ip, '$sp.host:$sp.port') or { 361 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 362 | return 363 | } 364 | 365 | if sp.config.verbose { 366 | print('\x1b[2J\x1b[1;1H') 367 | println('[\x1b[32;1mspaceship\x1b[0m] ready for take off 🚀 (http://$sp.host:$sp.port/)\n') 368 | } 369 | 370 | for { 371 | mut conn := ln.accept() or { 372 | eprintln('[\x1b[31;1merror\x1b[0m] $err') 373 | continue 374 | } 375 | 376 | go sp.handle_connection(mut conn) 377 | } 378 | } -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'spaceship' 3 | description: 'Spaceship is a web framework made in V' 4 | version: '0.1.0' 5 | license: 'MIT' 6 | dependencies: [] 7 | } 8 | --------------------------------------------------------------------------------