]>
Commit | Line | Data |
---|---|---|
8b82926a MT |
1 | #!/usr/bin/python3 |
2 | ||
539d08da | 3 | import asyncio |
8b82926a | 4 | import datetime |
539d08da MT |
5 | import io |
6 | import logging | |
e3bc8f21 | 7 | import magic |
539d08da MT |
8 | import tornado.iostream |
9 | import tornado.tcpserver | |
66862195 | 10 | |
539d08da | 11 | from . import base |
11347e46 | 12 | from .misc import Object |
672150b2 | 13 | from .decorators import * |
66862195 | 14 | |
539d08da MT |
15 | # Setup logging |
16 | log = logging.getLogger(__name__) | |
17 | ||
18 | CHUNK_SIZE = 1024 ** 2 | |
19 | ||
66862195 | 20 | class Nopaste(Object): |
672150b2 MT |
21 | def _get_paste(self, query, *args, **kwargs): |
22 | return self.db.fetch_one(Paste, query, *args, **kwargs) | |
23 | ||
e3bc8f21 | 24 | def create(self, content, subject=None, mimetype=None, expires=None, account=None, address=None): |
2268f20b MT |
25 | # Store the blob |
26 | blob_id = self._store_blob(content) | |
27 | ||
e3bc8f21 MT |
28 | # Guess the mimetype if none set |
29 | if not mimetype: | |
30 | mimetype = magic.from_buffer(content, mime=True) | |
31 | ||
66862195 MT |
32 | uid = None |
33 | if account: | |
34 | uid = account.uid | |
35 | ||
8b82926a MT |
36 | if expires: |
37 | expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires) | |
38 | ||
66862195 | 39 | # http://blog.00null.net/easily-generating-random-strings-in-postgresql/ |
672150b2 | 40 | paste = self._get_paste(""" |
2268f20b MT |
41 | INSERT INTO |
42 | nopaste | |
43 | ( | |
44 | uuid, | |
45 | subject, | |
46 | content, | |
47 | time_expires, | |
48 | address, | |
49 | uid, | |
50 | mimetype, | |
51 | size, | |
52 | blob_id | |
53 | ) | |
54 | VALUES | |
55 | ( | |
56 | random_slug(), %s, %s, %s, %s, %s, %s, %s, %s | |
57 | ) | |
58 | RETURNING | |
672150b2 | 59 | * |
2268f20b MT |
60 | """, subject, content, expires or None, address, uid, mimetype, len(content), blob_id, |
61 | ) | |
66862195 | 62 | |
672150b2 MT |
63 | # Log result |
64 | log.info("Created a new paste (%s) of %s byte(s) from %s (%s - %s)" % ( | |
65 | paste.uuid, paste.size, paste.address, paste.asn or "N/A", paste.country or "N/A", | |
66 | )) | |
67 | ||
68 | return paste | |
69 | ||
70 | def _fetch_blob(self, id): | |
71 | blob = self.db.get(""" | |
72 | SELECT | |
73 | data | |
74 | FROM | |
75 | nopaste_blobs | |
76 | WHERE | |
77 | id = %s | |
78 | """, id, | |
79 | ) | |
80 | ||
81 | if blob: | |
82 | return blob.data | |
66862195 | 83 | |
2268f20b MT |
84 | def _store_blob(self, data): |
85 | """ | |
86 | Stores the blob by sending it to the database and returning its ID | |
87 | """ | |
88 | blob = self.db.get(""" | |
89 | INSERT INTO | |
90 | nopaste_blobs | |
91 | ( | |
92 | data | |
93 | ) | |
94 | VALUES | |
95 | ( | |
96 | %s | |
97 | ) | |
98 | ON CONFLICT | |
99 | ( | |
100 | data | |
101 | ) | |
102 | DO UPDATE SET | |
103 | last_uploaded_at = CURRENT_TIMESTAMP | |
104 | RETURNING | |
105 | id | |
106 | """, data | |
107 | ) | |
108 | ||
109 | # Return the ID | |
110 | return blob.id | |
111 | ||
66862195 | 112 | def get(self, uuid): |
672150b2 MT |
113 | paste = self._get_paste(""" |
114 | SELECT | |
115 | * | |
116 | FROM | |
117 | nopaste | |
118 | WHERE | |
119 | uuid = %s | |
120 | AND ( | |
121 | expires_at >= CURRENT_TIMESTAMP | |
122 | OR | |
123 | expires_at IS NULL | |
124 | ) | |
125 | """, uuid, | |
126 | ) | |
66862195 | 127 | |
672150b2 | 128 | return paste |
66862195 | 129 | |
0de07bc3 | 130 | def get_content(self, uuid): |
09fa25a8 MT |
131 | res = self.db.get("SELECT content FROM nopaste \ |
132 | WHERE uuid = %s", uuid) | |
0de07bc3 | 133 | |
09fa25a8 | 134 | if res: |
a41085fb | 135 | return bytes(res.content) |
0de07bc3 | 136 | |
66862195 | 137 | def _update_lastseen(self, uuid): |
bfbf061b | 138 | self.db.execute("UPDATE nopaste SET time_lastseen = NOW(), views = views + 1 \ |
66862195 MT |
139 | WHERE uuid = %s", uuid) |
140 | ||
063fd092 MT |
141 | def cleanup(self): |
142 | """ | |
143 | Removes all expired pastes and removes any unneeded blobs | |
144 | """ | |
145 | # Remove all expired pastes | |
146 | self.db.execute(""" | |
147 | DELETE FROM | |
148 | nopaste | |
149 | WHERE | |
150 | expires_at < CURRENT_TIMESTAMP | |
151 | """) | |
152 | ||
153 | # Remove unneeded blobs | |
154 | self.db.execute(""" | |
155 | DELETE FROM | |
156 | nopaste_blobs | |
157 | WHERE NOT EXISTS | |
158 | ( | |
159 | SELECT | |
160 | 1 | |
161 | FROM | |
162 | nopaste | |
163 | WHERE | |
164 | nopaste.blob_id = nopaste_blobs.id | |
165 | ) | |
166 | """) | |
539d08da MT |
167 | |
168 | ||
169 | class Paste(Object): | |
170 | def init(self, id, data): | |
171 | self.id, self.data = id, data | |
172 | ||
173 | # UUID | |
174 | ||
175 | @property | |
176 | def uuid(self): | |
177 | return self.data.uuid | |
178 | ||
179 | # Subject | |
180 | ||
181 | @property | |
182 | def subject(self): | |
183 | return self.data.subject | |
184 | ||
185 | # Created At | |
186 | ||
187 | @property | |
188 | def created_at(self): | |
189 | return self.data.created_at | |
190 | ||
191 | time_created = created_at | |
192 | ||
193 | # Expires At | |
194 | ||
195 | @property | |
196 | def expires_at(self): | |
197 | return self.data.expires_at | |
198 | ||
199 | time_expires = expires_at | |
200 | ||
201 | # Blob | |
202 | ||
203 | @lazy_property | |
204 | def blob(self): | |
205 | return self.backend.nopaste._fetch_blob(self.data.blob_id) | |
206 | ||
207 | content = blob | |
208 | ||
209 | # Size | |
210 | ||
211 | @property | |
212 | def size(self): | |
213 | return self.data.size | |
214 | ||
672150b2 MT |
215 | # MIME Type |
216 | ||
217 | @property | |
218 | def mimetype(self): | |
219 | return self.data.mimetype or "application/octet-stream" | |
220 | ||
539d08da MT |
221 | # Address |
222 | ||
223 | @property | |
224 | def address(self): | |
225 | return self.data.address | |
226 | ||
227 | # Location | |
228 | ||
229 | @lazy_property | |
230 | def location(self): | |
231 | return self.backend.location.lookup("%s" % self.address) | |
232 | ||
233 | # ASN | |
234 | ||
235 | @lazy_property | |
236 | def asn(self): | |
237 | if self.location and self.location.asn: | |
238 | return self.backend.location.get_asn(self.location.asn) | |
239 | ||
240 | # Country | |
241 | ||
242 | @lazy_property | |
243 | def country(self): | |
244 | if self.location and self.location.country_code: | |
245 | return self.backend.location.get_country(self.location.country_code) | |
246 | ||
672150b2 MT |
247 | # Legacy |
248 | ||
249 | @property | |
250 | def account(self): | |
251 | return None | |
252 | ||
539d08da MT |
253 | |
254 | class Service(tornado.tcpserver.TCPServer): | |
255 | def __init__(self, config, **kwargs): | |
256 | # Initialize backend | |
257 | self.backend = base.Backend(config) | |
258 | ||
259 | super().__init__(**kwargs) | |
260 | ||
261 | async def handle_stream(self, stream, address): | |
262 | buffer = io.BytesIO() | |
263 | ||
264 | # Read the entire stream | |
265 | try: | |
266 | while True: | |
267 | chunk = await stream.read_bytes(CHUNK_SIZE, partial=True) | |
268 | ||
269 | log.debug("Read a chunk of %s byte(s)" % len(chunk)) | |
270 | ||
271 | # Write the chunk into the buffer | |
272 | buffer.write(chunk) | |
273 | ||
274 | # If we have read less then we have reached the end | |
275 | if len(chunk) < CHUNK_SIZE: | |
276 | break | |
277 | ||
278 | # End if the stream closed unexpectedly | |
279 | except tornado.iostream.StreamClosedError as e: | |
280 | return | |
281 | ||
282 | log.debug("Finished reading payload") | |
283 | ||
284 | # Process address | |
285 | address, port = address | |
286 | ||
287 | # Store this into the database | |
288 | with self.backend.db.transaction(): | |
289 | uuid = self.backend.nopaste.create( | |
290 | buffer.getvalue(), | |
291 | subject="Streamed Upload", | |
292 | address=address, | |
293 | ) | |
294 | ||
295 | # Format a response message | |
296 | message = "https://nopaste.ipfire.org/view/%s\n" % uuid | |
297 | ||
298 | # Send the message | |
299 | await stream.write(message.encode("utf-8")) | |
300 | ||
301 | # We are done, close the stream | |
302 | stream.close() |