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
|
|
|
if endpoint.length >= 2 |
82
|
|
|
ep_file = endpoint[1].split("?")[0] |
83
|
|
|
else |
84
|
|
|
ep_file = "" |
85
|
|
|
end |
86
|
|
|
|
87
|
|
|
method = request[0] |
88
|
|
|
endpoints = [] # Valid endpoints |
89
|
|
|
|
90
|
|
|
# Load the JSON served files |
91
|
|
|
@config["serves"].each do |js| |
92
|
|
|
# The filename is a valid endpoint |
93
|
|
|
endpoints.push File.basename(js, ".*") |
94
|
|
|
end |
95
|
|
|
|
96
|
|
|
# Load the endpoints |
97
|
|
|
found = false |
98
|
|
|
endpoints.each do |ep| |
99
|
|
|
if ep_file == ep |
100
|
|
|
found = true |
101
|
|
|
MIDB::Interface::Server.info(:match_json, ep) |
102
|
|
|
# Create the model |
103
|
|
|
dbop = MIDB::API::Model.new(ep, @db, self) |
104
|
|
|
# Analyze the request and pass it to the model |
105
|
|
|
# Is the method accepted? |
106
|
|
|
accepted_methods = ["GET", "POST", "PUT", "DELETE"] |
107
|
|
|
unless accepted_methods.include? method |
108
|
|
|
@http_status = "405 Method Not Allowed" |
109
|
|
|
response_json = MIDB::Interface::Server.json_error(405, "Method Not Allowed").to_json |
110
|
|
|
else |
111
|
|
|
# Do we need authentication? |
112
|
|
|
auth_req = false |
113
|
|
|
unauthenticated = false |
114
|
|
|
if @config["privacy#{method.downcase}"] == true |
115
|
|
|
MIDB::Interface::Server.info(:auth_required) |
116
|
|
|
auth_req = true |
117
|
|
|
|
118
|
|
|
# For GET and DELETE requests, the object of the digest is the endpoint |
119
|
|
|
if (method == "GET") || (method == "DELETE") |
120
|
|
|
data = ep_file |
121
|
|
|
end |
122
|
|
|
|
123
|
|
|
# If it's a GET request and we have a different key for GET methods... |
124
|
|
|
if (@config["apigetkey"] != nil) && (method == "GET") |
125
|
|
|
unauthenticated = (not headers.has_key? "Authentication") || |
126
|
|
|
(not MIDB::API::Security.check?(headers["Authentication"], data, @config["apigetkey"])) |
127
|
|
|
else |
128
|
|
|
unauthenticated = (not headers.has_key? "Authentication") || |
129
|
|
|
(not MIDB::API::Security.check?(headers["Authentication"], data, @config["apikey"])) |
130
|
|
|
end |
131
|
|
|
end |
132
|
|
|
# Proceed to handle the request |
133
|
|
|
if unauthenticated |
134
|
|
|
response_json = self.unauth_request |
135
|
|
|
puts ">> has header: #{headers.has_key? "Authentication"}" |
136
|
|
|
else |
137
|
|
|
MIDB::Interface::Server.info(:auth_success) if (not unauthenticated) && auth_req |
138
|
|
|
if method == "GET" |
139
|
|
|
case endpoint.length |
140
|
|
|
when 2 |
141
|
|
|
# No ID has been specified. Return all the entries |
142
|
|
|
# Pass it to the model and get the JSON |
143
|
|
|
MIDB::Interface::Server.info(:fetch, "get_all_entries()") |
144
|
|
|
response_json = dbop.get_all_entries().to_json |
145
|
|
|
when 3 |
146
|
|
|
# This regular expression checks if it contains an integer |
147
|
|
|
if /\A[-+]?\d+\z/ === endpoint[2] |
|
|
|
|
148
|
|
|
# An ID has been specified. Should it exist, return all of its entries. |
149
|
|
|
MIDB::Interface::Server.info(:fetch, "get_entries(#{endpoint[2]})") |
150
|
|
|
response_json = dbop.get_entries(endpoint[2].to_i).to_json |
151
|
|
|
else |
152
|
|
|
# A row has been specified, but no pattern |
153
|
|
|
MIDB::Interface::Server.info(:fetch, "get_column_entries(#{endpoint[2]})") |
154
|
|
|
response_json = dbop.get_column_entries(endpoint[2]).to_json |
155
|
|
|
end |
156
|
|
|
when 4 |
157
|
|
|
if (endpoint[2].is_a? String) && (endpoint[3].is_a? String) then |
158
|
|
|
# A row and a pattern have been specified |
159
|
|
|
MIDB::Interface::Server.info(:fetch, "get_matching_rows(#{endpoint[2]}, #{endpoint[3]})") |
160
|
|
|
response_json = dbop.get_matching_rows(endpoint[2], endpoint[3]).to_json |
161
|
|
|
end |
162
|
|
|
end |
163
|
|
|
elsif method == "POST" |
164
|
|
|
MIDB::Interface::Server.info(:fetch, "post(#{data})") |
165
|
|
|
response_json = dbop.post(data).to_json |
166
|
|
|
else |
167
|
|
|
if endpoint.length >= 3 |
168
|
|
|
if method == "DELETE" |
169
|
|
|
MIDB::Interface::Server.info(:fetch, "delete(#{endpoint[2]})") |
170
|
|
|
response_json = dbop.delete(endpoint[2]).to_json |
171
|
|
|
elsif method == "PUT" |
172
|
|
|
MIDB::Interface::Server.info(:fetch, "put(#{endpoint[2]}, data)") |
173
|
|
|
response_json = dbop.put(endpoint[2], data).to_json |
174
|
|
|
end |
175
|
|
|
else |
176
|
|
|
@http_status = "404 Not Found" |
177
|
|
|
response_json = MIDB::Interface::Server.json_error(404, "Must specify an ID.").to_json |
178
|
|
|
end |
179
|
|
|
end |
180
|
|
|
end |
181
|
|
|
end |
182
|
|
|
MIDB::Interface::Server.info(:response, response_json) |
183
|
|
|
# Return the results via HTTP |
184
|
|
|
socket.print "HTTP/1.1 #{@http_status}\r\n" + |
185
|
|
|
"Content-Type: text/json\r\n" + |
186
|
|
|
"Content-Length: #{response_json.size}\r\n" + |
187
|
|
|
"Connection: close\r\n" |
188
|
|
|
socket.print "\r\n" |
189
|
|
|
socket.print response_json |
190
|
|
|
socket.print "\r\n" |
191
|
|
|
MIDB::Interface::Server.info(:success) |
192
|
|
|
end |
193
|
|
|
end |
194
|
|
|
unless found |
195
|
|
|
MIDB::Interface::Server.info(:not_found) |
196
|
|
|
response = MIDB::Interface::Server.json_error(404, "Invalid API endpoint.").to_json |
197
|
|
|
|
198
|
|
|
socket.print "HTTP/1.1 404 Not Found\r\n" + |
199
|
|
|
"Content-Type: text/json\r\n" + |
200
|
|
|
"Content-Length: #{response.size}\r\n" + |
201
|
|
|
"Connection: close\r\n" |
202
|
|
|
socket.print "\r\n" |
203
|
|
|
socket.print response |
204
|
|
|
end |
205
|
|
|
end |
206
|
|
|
end |
207
|
|
|
|
208
|
|
|
# Method: parse_request |
209
|
|
|
# Parses an HTTP requests and returns an array [method, uri] |
210
|
|
|
def parse_request(req) |
211
|
|
|
[req.split(" ")[0], req.split(" ")[1]] |
212
|
|
|
end |
213
|
|
|
end |
214
|
|
|
end |
215
|
|
|
end |