Repository navigation
Expand file tree
/
Copy pathelib.cgi
More file actions
executable file
·147 lines (134 loc) · 4.6 KB
/
Copy pathelib.cgi
File metadata and controls
executable file
·147 lines (134 loc) · 4.6 KB
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
#!/usr/bin/env python3
"""Read-only bibliography lookup. POST a JSON array of citation tags."""
import json
import os
import sys
import traceback
from pathlib import Path
MAX_BODY_BYTES = 128 * 1024
MAX_TAGS = 1000
MAX_TAG_LENGTH = 255
CONFIG_PATH = Path(os.environ.get("ELIB_CONFIG", "/etc/elib.json"))
FIELDS = (
"bibtag",
"bibtype",
"author",
"chapter",
"title",
"publisher",
"journal",
"booktitle",
"eprint",
"school",
"number",
"volume",
"pages",
"month",
"year",
"isPublic",
)
class RequestError(Exception):
def __init__(self, status, message):
super().__init__(message)
self.status = status
def read_tags(environ, stream):
if environ.get("REQUEST_METHOD") != "POST":
raise RequestError(
"405 Method Not Allowed", "Use POST with a JSON array of tags."
)
if (
environ.get("CONTENT_TYPE", "").split(";")[0].strip().lower()
!= "application/json"
):
raise RequestError("415 Unsupported Media Type", "Expected application/json.")
try:
length = int(environ.get("CONTENT_LENGTH", ""))
except ValueError:
raise RequestError("400 Bad Request", "Invalid Content-Length.") from None
if not 0 < length <= MAX_BODY_BYTES:
raise RequestError(
"413 Content Too Large", "Request body must be 1–131072 bytes."
)
body = stream.read(length)
if len(body) != length:
raise RequestError("400 Bad Request", "Incomplete request body.")
try:
tags = json.loads(body)
except (ValueError, UnicodeDecodeError):
raise RequestError("400 Bad Request", "Invalid JSON.") from None
if not isinstance(tags, list) or len(tags) > MAX_TAGS:
raise RequestError("400 Bad Request", "Expected an array of at most 1000 tags.")
if any(
not isinstance(tag, str)
or not tag
or len(tag) > MAX_TAG_LENGTH
or tag != tag.strip()
or any(ord(char) < 32 for char in tag)
for tag in tags
):
raise RequestError(
"400 Bad Request",
"Tags must be nonempty strings without surrounding whitespace or control characters.",
)
return list(dict.fromkeys(tags))
def lookup(tags):
if not tags:
return {"entries": {}, "missing": []}
import mysql.connector
config = json.loads(CONFIG_PATH.read_text())
config.update(
host="mysql.csail.mit.edu",
database="elib",
connection_timeout=10,
read_timeout=15,
write_timeout=15,
)
connection = mysql.connector.connect(**config)
try:
cursor = connection.cursor(dictionary=True)
try:
# Private paper URLs never leave the database.
columns = ", ".join("`" + field + "`" for field in FIELDS)
placeholders = ", ".join(["%s"] * len(tags))
cursor.execute(
f"SELECT {columns}, CASE WHEN isPublic THEN url ELSE NULL END AS url "
f"FROM bibtex WHERE bibtag IN ({placeholders})",
tuple(tags),
)
rows = {row["bibtag"]: row for row in cursor.fetchall()}
finally:
cursor.close()
finally:
connection.close()
# Return exact requested tags only, even if the DB collation ignores case.
entries = {tag: rows[tag] for tag in tags if tag in rows}
return {"entries": entries, "missing": [tag for tag in tags if tag not in entries]}
def main():
status = "200 OK"
try:
result = lookup(read_tags(os.environ, sys.stdin.buffer))
body = json.dumps(result, ensure_ascii=True)
except RequestError as error:
status = error.status
body = json.dumps({"error": str(error)})
except Exception:
traceback.print_exc(file=sys.stderr)
status = "503 Service Unavailable"
body = json.dumps({"error": "Bibliography lookup unavailable."})
headers = [
f"Status: {status}",
"Content-Type: application/json; charset=utf-8",
"Cache-Control: no-store",
"X-Content-Type-Options: nosniff",
]
if status.startswith("405"):
headers.append("Allow: POST")
sys.stdout.write("\r\n".join(headers) + "\r\n\r\n" + body + "\n")
if __name__ == "__main__":
# Apache may start us with system Python. Use the consuming repository's
# virtual environment without embedding a project-specific absolute path.
venv = Path(__file__).resolve().parents[2] / ".venv"
if Path(sys.prefix).resolve() != venv.resolve():
python = str(venv / "bin" / "python")
os.execv(python, [python, str(Path(__file__).resolve())])
main()