-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
209 lines (181 loc) · 5.08 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
const express = require('express')
const fetch = require('node-fetch')
const sqlite3 = require('sqlite3').verbose()
const btoa = require('btoa')
const log4js = require('log4js')
const config = require('./config.json')
// globals
var authstring = 'Basic ' + btoa(config.github_username+':'+config.github_pat)
// rate limit control
var rl_remaining = -1
var rl_limit = -1
var rl_used = -1
var rl_rest = -1
// sql statements
const cache_table_definition = `CREATE TABLE IF NOT EXISTS cache (
id TEXT PRIMARY KEY,
user TEXT,
last INTEGER,
body BLOB
)`
const cache_get_query = `SELECT body, last FROM cache WHERE
id = ?
`
const cache_store_query = `REPLACE INTO cache(
id,
user,
last,
body
)
VALUES(?,?,?,?)
`
// FUNCTIONS
// quickly send errors back to the client
function errorout(res,msg,status) {
res.status(status)
res.send('{"error":"'+msg+'"}')
logger.info(msg.toUpperCase())
}
// initial function trying to get data from the cache database
function get(res,id,user) {
db.get(cache_get_query, [id], (err, row) => {
if(err) {
logger.error("CACHE DB ERROR")
gist_get_meta(res,id,user)
return
}
if(!row) {
logger.error("DB NO CACHED DATA AVAILABLE")
gist_get_meta(res,id,user)
return
}
let now = Math.round(Date.now() / 1000)
let age = parseInt(now)-parseInt(row.last)
logger.info("SENDING CACHED DATA (AGE "+age+" SEC)")
res.send(row.body)
return
})
}
// save results to the cache database
function store(res,id,user,body) {
let now = Math.round(Date.now() / 1000)
db.run(cache_store_query, [id,user,now,body], function(err) {
if(err) {
logger.info(err.message)
return false
}
return true
})
}
function gist_get_meta(res,id,user) {
let githuburl = 'https://api.github.com/gists/' + id
let options = {
method: 'GET',
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: authstring,
'User-agent': 'strollgistid'
}
}
fetch(githuburl,options)
.then(result => {
rl_limit = result.headers.get("x-ratelimit-limit")
rl_remaining = result.headers.get("x-ratelimit-remaining")
rl_reset = result.headers.get("x-ratelimit-reset")
rl_used = result.headers.get("x-ratelimit-used")
let now = Math.round(Date.now() / 1000)
let rtime = Math.ceil((parseInt(rl_reset)-parseInt(now))/60)
logger.info("REMAINING RATE: "+rl_remaining+" ("+rl_used+"/"+rl_limit+"), NEXT RESET IN LESS THAN "+rtime+" MIN")
result.json().then(function(json) {
let rawurl = false
try {
rawurl = json.files.strollview.raw_url
} catch (e) {
if(rl_remaining<1) {
errorout(res,"github api rate limit reached",503)
return
} else {
errorout(res,"error getting raw url",500)
return
}
return
}
gist_get_raw(res,id,user,rawurl)
})
})
.catch(err => logger.error(err))
}
function gist_get_raw(res,id,user,rawurl) {
const options = {
method: 'GET',
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: authstring,
'User-agent': 'strollgistid'
}
}
fetch(rawurl,options)
.then(result => result.text())
.then(body => {
deliver(res,id,user,body)
})
.catch(err => logger.error(err))
}
function deliver(res,id,user,body) {
logger.info("SENDING FRESH DATA AND UPDATING CACHE")
store(res,id,user,body)
res.status(200)
res.send(body)
return
}
// configure logger
log4js.configure({
appenders: {
svfile: { type: 'file', filename: '/var/log/strollgistid.log' },
svout: { type: 'stdout' }
},
categories: { default: { appenders: ['svfile','svout'], level: 'info' } }
})
const logger = log4js.getLogger()
logger.level = 'INFO'
// init database
const db = new sqlite3.Database('cache.sqlite3')
db.run(cache_table_definition)
// init server
const app = express()
app.all('*', function (req, res, next) {
logger.info('PROCESSING REQUEST ' + req.path)
const pathelements = req.path.split('/')
if (Object.keys(pathelements).length !== 3) {
logger.warn('MALFORMED URL, SENDING 404')
res.status(404)
res.send('{"error":"malformed url"}')
return
}
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', '*')
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
res.header('Content-Type', 'application/json')
const user = pathelements[1]
const id = pathelements[2]
let regex_user = new RegExp('^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$')
let regex_id = new RegExp('^[0-9a-z]+$')
if ( !regex_user.test(user) || !regex_id.test(id) ) {
errorout(res,"insufficient IDs",404)
return
} else {
if (req.method === 'OPTIONS') {
logger.info('OPTIONS REQUEST ANSWERED')
res.status(200)
res.send()
} else {
if(req.header('X-SV-CACHE-UPDATE') === 'TRUE') {
gist_get_meta(res,id,user)
} else {
get(res,id,user)
}
}
}
})
app.listen(config.port,config.interface)
logger.info('Listening on '+config.interface+":"+config.port)