1
|
|
|
require 'midb/server_controller' |
2
|
|
|
require 'midb/server_model' |
3
|
|
|
require 'midb/server_view' |
4
|
|
|
require 'midb/errors_view' |
5
|
|
|
require 'midb/security_controller' |
6
|
|
|
require 'midb/hooks' |
7
|
|
|
|
8
|
|
|
require 'yaml' |
9
|
|
|
require 'socket' |
10
|
|
|
require 'uri' |
11
|
|
|
require 'json' |
12
|
|
|
require 'sqlite3' |
13
|
|
|
|
14
|
|
|
module MIDB |
15
|
|
|
module API |
16
|
|
|
# @author unrar |
17
|
|
|
# This class handles runs the server engine using sockets and a loop. |
18
|
|
|
class Engine |
19
|
|
|
# Attribute declaration here |
20
|
|
|
# @!attribute config |
21
|
|
|
# @return [Hash] Contains the project's configuration, saved in .midb.yaml |
22
|
|
|
# @!attribute db |
23
|
|
|
# @return [String] Database name (if SQLite is the engine, file name without extension) |
24
|
|
|
# @!attribute http_status |
25
|
|
|
# @return [String] HTTP status code and string representation for the header |
26
|
|
|
# @!attribute h |
27
|
|
|
# @return [Object] MIDB::API::Hooks instance |
28
|
|
|
attr_accessor :config, :db, :http_status, :hooks |
29
|
|
|
|
30
|
|
|
# Handle an unauthorized request |
31
|
|
|
def unauth_request |
32
|
|
|
@http_status = "401 Unauthorized" |
33
|
|
|
MIDB::Interface::Server.info(:no_auth) |
34
|
|
|
MIDB::Interface::Server.json_error(401, "Unauthorized").to_json |
35
|
|
|
end |
36
|
|
|
|
37
|
|
|
# Constructor |
38
|
|
|
# |
39
|
|
|
# @param db [String] Database to which the server will bind. |
40
|
|
|
# @param stat [Fixnum] HTTP Status |
41
|
|
|
# @param cnf [Hash] Config from the server controller. |
42
|
|
|
def initialize(db, stat, cnf, hooks=nil) |
43
|
|
|
@config = cnf |
44
|
|
|
@db = db |
45
|
|
|
@http_status = stat |
46
|
|
|
if hooks == nil |
47
|
|
|
@hooks = MIDB::API::Hooks.new |
48
|
|
|
else |
49
|
|
|
@hooks = hooks |
50
|
|
|
end |
51
|
|
|
end |
52
|
|
|
|
53
|
|
|
# Starts the server on a given port (default: 8081) |
54
|
|
|
# |
55
|
|
|
# @param port [Fixnum] Port to which the server will listen. |
56
|
|
|
def start(port=8081) |
57
|
|
|
serv = TCPServer.new("localhost", port) |
58
|
|
|
MIDB::Interface::Server.info(:start, port) |
59
|
|
|
|
60
|
|
|
# Manage the requests |
61
|
|
|
loop do |
62
|
|
|
socket = serv.accept |
63
|
|
|
MIDB::Interface::Server.info(:incoming_request, socket.addr[3]) |
64
|
|
|
|
65
|
|
|
request = self.parse_request(socket.gets) |
66
|
|
|
|
67
|
|
|
# Get a hash with the headers |
68
|
|
|
headers = {} |
69
|
|
|
while line = socket.gets.split(' ', 2) |
|
|
|
|
70
|
|
|
break if line[0] == "" |
71
|
|
|
headers[line[0].chop] = line[1].strip |
72
|
|
|
end |
73
|
|
|
data = socket.read(headers["Content-Length"].to_i) |
74
|
|
|
|
75
|
|
|
|
76
|
|
|
MIDB::Interface::Server.info(:request, request) |
77
|
|
|
response_json = Hash.new() |
78
|
|
|
|
79
|
|
|
# Endpoint syntax: ["", FILE, ID, (ACTION)] |
80
|
|
|
endpoint = request[1].split("/") |
81
|
|
|
ep_file = endpoint[1].split("?")[0] |
82
|
|
|
|
83
|
|
|
method = request[0] |
84
|
|
|
endpoints = [] # Valid endpoints |
85
|
|
|
|
86
|
|
|
# Load the JSON served files |
87
|
|
|
@config["serves"].each do |js| |
88
|
|
|
# The filename is a valid endpoint |
89
|
|
|
endpoints.push File.basename(js, ".*") |
90
|
|
|
end |
91
|
|
|
|
92
|
|
|
# Load the endpoints |
93
|
|
|
found = false |
94
|
|
|
endpoints.each do |ep| |
95
|
|
|
if ep_file == ep |
96
|
|
|
found = true |
97
|
|
|
MIDB::Interface::Server.info(:match_json, ep) |
98
|
|
|
# Create the model |
99
|
|
|
dbop = MIDB::API::Model.new(ep, @db, self) |
100
|
|
|
# Analyze the request and pass it to the model |
101
|
|
|
# Is the method accepted? |
102
|
|
|
accepted_methods = ["GET", "POST", "PUT", "DELETE"] |
103
|
|
|
unless accepted_methods.include? method |
104
|
|
|
@http_status = "405 Method Not Allowed" |
105
|
|
|
response_json = MIDB::Interface::Server.json_error(405, "Method Not Allowed").to_json |
106
|
|
|
else |
107
|
|
|
# Do we need authentication? |
108
|
|
|
auth_req = false |
109
|
|
|
unauthenticated = false |
110
|
|
|
if @config["privacy#{method.downcase}"] == true |
111
|
|
|
MIDB::Interface::Server.info(:auth_required) |
112
|
|
|
auth_req = true |
113
|
|
|
# If it's a GET request and we have a different key for GET methods... |
114
|
|
|
if method == "GET" |
115
|
|
|
data = ep_file |
116
|
|
|
end |
117
|
|
|
if (@config["apigetkey"] != nil) && (method == "GET") |
118
|
|
|
unauthenticated = (not headers.has_key? "Authentication") || |
119
|
|
|
(not MIDB::API::Security.check?(headers["Authentication"], data, @config["apigetkey"])) |
120
|
|
|
else |
121
|
|
|
unauthenticated = (not headers.has_key? "Authentication") || |
122
|
|
|
(not MIDB::API::Security.check?(headers["Authentication"], data, @config["apikey"])) |
123
|
|
|
end |
124
|
|
|
end |
125
|
|
|
# Proceed to handle the request |
126
|
|
|
if unauthenticated |
127
|
|
|
response_json = self.unauth_request |
128
|
|
|
else |
129
|
|
|
MIDB::Interface::Server.info(:auth_success) if (not unauthenticated) && auth_req |
130
|
|
|
if method == "GET" |
131
|
|
|
case endpoint.length |
132
|
|
|
when 2 |
133
|
|
|
# No ID has been specified. Return all the entries |
134
|
|
|
# Pass it to the model and get the JSON |
135
|
|
|
response_json = dbop.get_all_entries().to_json |
136
|
|
|
when 3 |
137
|
|
|
# An ID has been specified. Should it exist, return all of its entries. |
138
|
|
|
response_json = dbop.get_entries(endpoint[2]).to_json |
139
|
|
|
end |
140
|
|
|
elsif method == "POST" |
141
|
|
|
response_json = dbop.post(data).to_json |
142
|
|
|
else |
143
|
|
|
if endpoint.length >= 3 |
144
|
|
|
if method == "DELETE" |
145
|
|
|
response_json = dbop.delete(endpoint[2]).to_json |
146
|
|
|
elsif method == "PUT" |
147
|
|
|
response_json = dbop.put(endpoint[2], data).to_json |
148
|
|
|
end |
149
|
|
|
else |
150
|
|
|
@http_status = "404 Not Found" |
151
|
|
|
response_json = MIDB::Interface::Server.json_error(404, "Must specify an ID.").to_json |
152
|
|
|
end |
153
|
|
|
end |
154
|
|
|
end |
155
|
|
|
end |
156
|
|
|
MIDB::Interface::Server.info(:response, response_json) |
157
|
|
|
# Return the results via HTTP |
158
|
|
|
socket.print "HTTP/1.1 #{@http_status}\r\n" + |
159
|
|
|
"Content-Type: text/json\r\n" + |
160
|
|
|
"Content-Length: #{response_json.size}\r\n" + |
161
|
|
|
"Connection: close\r\n" |
162
|
|
|
socket.print "\r\n" |
163
|
|
|
socket.print response_json |
164
|
|
|
socket.print "\r\n" |
165
|
|
|
MIDB::Interface::Server.info(:success) |
166
|
|
|
end |
167
|
|
|
end |
168
|
|
|
unless found |
169
|
|
|
MIDB::Interface::Server.info(:not_found) |
170
|
|
|
response = MIDB::Interface::Server.json_error(404, "Invalid API endpoint.").to_json |
171
|
|
|
|
172
|
|
|
socket.print "HTTP/1.1 404 Not Found\r\n" + |
173
|
|
|
"Content-Type: text/json\r\n" + |
174
|
|
|
"Content-Length: #{response.size}\r\n" + |
175
|
|
|
"Connection: close\r\n" |
176
|
|
|
socket.print "\r\n" |
177
|
|
|
socket.print response |
178
|
|
|
end |
179
|
|
|
end |
180
|
|
|
end |
181
|
|
|
|
182
|
|
|
# Method: parse_request |
183
|
|
|
# Parses an HTTP requests and returns an array [method, uri] |
184
|
|
|
def parse_request(req) |
185
|
|
|
[req.split(" ")[0], req.split(" ")[1]] |
186
|
|
|
end |
187
|
|
|
end |
188
|
|
|
end |
189
|
|
|
end |