" ====================================================================== " File: autoload/http.vim " Description: HTTP server that runs in Vim, via netcat. " Maintainer: Thomas Allen " " Copyright (c) 2018 Thomas Allen " " Permission is hereby granted, free of charge, to any person obtaining " a copy of this software and associated documentation files (the " "Software"), to deal in the Software without restriction, including " without limitation the rights to use, copy, modify, merge, publish, " distribute, sublicense, and/or sell copies of the Software, and to " permit persons to whom the Software is furnished to do so, subject to " the following conditions: " " The above copyright notice and this permission notice shall be " included in all copies or substantial portions of the Software. " " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, " EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. " IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY " CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, " TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE " SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " ====================================================================== function! http#new_server(port) let server = {} let server.port = a:port let server.handlers = {} function server.log(msg) echomsg 'http: ' . string(a:msg) endfunction function server.start() let cmd = ['nc', '-k', '-l', '-p', self.port] let options = {} let options.in_mode = 'raw' let options.out_mode = 'raw' let options.callback = function(self.on_request, self) let self.job = job_start(cmd, options) if job_status(self.job) == 'run' call self.log('Listening on ' . self.port . '...') else call self.log('Failed to start') endif endfunction function server.stop() call self.log('Stopping') if exists('self.job') && job_status(self.job) == 'run' call job_stop(self.job) unlet self.job endif endfunction function server.on_request(chan, msg) let state = s:state_init let lines = split(a:msg, "\n") let req = {} let req.method = '' let req.path = '' let req.proto = '' let req.headers = {} let req.body = '' for line in lines if state == s:state_init let parts = split(trim(line), "\\s\\+") let req.method = parts[0] let req.path = parts[1] let req.proto = parts[2] let state = s:state_headers elseif state == s:state_headers let line = trim(line) if !empty(line) let i = stridx(line, ':') let k = line[:i - 1] let v = line[i + 1:] let req.headers[k] = v else if !(req.method == 'POST' || req.method == 'PUT') break endif let state = s:state_body endif elseif state == s:state_body let req.body .= line . "\n" endif endfor call self.process(req) endfunction function server.handle(method, path, handler) let self.handlers[a:method] = add(self.handlers[a:method], ['^' . a:path . '$', a:handler]) endfunction for method in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'] let server.handlers[method] = [] let server[tolower(method)] = function(server.handle, [method]) endfor function server.respond(status, headers, body) let headers = copy(a:headers) let body = a:body let t = type(body) if !exists('headers["Content-Type"]') if t == v:t_dict || t == v:t_list let body = json_encode(body) let headers['Content-Type'] = 'application/json' else let headers['Content-Type'] = 'text/plain' endif endif let headers['Content-Length'] = len(body) let resp = 'HTTP/1.1 ' . a:status . ' ' . s:status_messages[a:status] . "\n" for header in keys(headers) let resp .= header . ': ' . headers[header] . "\n" endfor let resp .= "\n" if !empty(body) let resp .= body endif let chan = job_getchannel(self.job) call ch_sendraw(chan, resp) endfunction function server.process(req) for [patt, Handler] in self.handlers[a:req.method] if match(a:req.path, patt) != -1 call Handler(self, a:req) return endif endfor call self.respond(404, {}, 'Resource not found') endfunction return server endfunction let s:state_init = 0 let s:state_headers = 1 let s:state_body = 2 let s:status_messages = { \ 100: 'Continue', \ 101: 'Switching Protocols', \ 200: 'OK', \ 201: 'Created', \ 202: 'Accepted', \ 203: 'Non-Authoritative Information', \ 204: 'No Content', \ 205: 'Reset Content', \ 206: 'Partial Content', \ 300: 'Multiple Choices', \ 301: 'Moved Permanently', \ 302: 'Found', \ 303: 'See Other', \ 304: 'Not Modified', \ 305: 'Use Proxy', \ 307: 'Temporary Redirect', \ 400: 'Bad Request', \ 401: 'Unauthorized', \ 402: 'Payment Required', \ 403: 'Forbidden', \ 404: 'Not Found', \ 405: 'Method Not Allowed', \ 406: 'Not Acceptable', \ 407: 'Proxy Authentication Required', \ 408: 'Request Time-out', \ 409: 'Conflict', \ 410: 'Gone', \ 411: 'Length Required', \ 412: 'Precondition Failed', \ 413: 'Request Entity Too Large', \ 414: 'Request-URI Too Large', \ 415: 'Unsupported Media Type', \ 416: 'Requested range not satisfiable', \ 417: 'Expectation Failed', \ 500: 'Internal Server Error', \ 501: 'Not Implemented', \ 502: 'Bad Gateway', \ 503: 'Service Unavailable', \ 504: 'Gateway Time-out', \ 505: 'HTTP Version not supported' \ }