| author | nipkow |
| Thu, 03 Feb 2022 10:33:55 +0100 | |
| changeset 75061 | 57df04e4f018 |
| parent 74945 | 4dc90b43ba94 |
| child 75104 | 08bb0d32b2e3 |
| permissions | -rw-r--r-- |
| 65077 | 1 |
/* Title: Pure/General/http.scala |
| 63823 | 2 |
Author: Makarius |
3 |
||
| 73438 | 4 |
HTTP client and server support. |
| 63823 | 5 |
*/ |
6 |
||
7 |
package isabelle |
|
8 |
||
9 |
||
| 73417 | 10 |
import java.io.{File => JFile}
|
11 |
import java.net.{InetSocketAddress, URI, URL, URLConnection, HttpURLConnection}
|
|
| 63823 | 12 |
import com.sun.net.httpserver.{HttpExchange, HttpHandler, HttpServer}
|
13 |
||
14 |
||
| 65077 | 15 |
object HTTP |
| 63823 | 16 |
{
|
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
17 |
/** content **/ |
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
18 |
|
| 73417 | 19 |
val mime_type_bytes: String = "application/octet-stream" |
20 |
val mime_type_text: String = "text/plain; charset=utf-8" |
|
21 |
val mime_type_html: String = "text/html; charset=utf-8" |
|
22 |
||
23 |
val default_mime_type: String = mime_type_bytes |
|
24 |
val default_encoding: String = UTF8.charset_name |
|
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
25 |
|
| 73417 | 26 |
sealed case class Content( |
27 |
bytes: Bytes, |
|
28 |
file_name: String = "", |
|
29 |
mime_type: String = default_mime_type, |
|
|
73429
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
30 |
encoding: String = default_encoding, |
|
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
31 |
elapsed_time: Time = Time.zero) |
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
32 |
{
|
| 73440 | 33 |
def text: String = new String(bytes.array, encoding) |
| 74945 | 34 |
def json: JSON.T = JSON.parse(text) |
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
35 |
} |
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
36 |
|
| 73417 | 37 |
def read_content(file: JFile): Content = |
38 |
{
|
|
39 |
val bytes = Bytes.read(file) |
|
40 |
val file_name = file.getName |
|
41 |
val mime_type = |
|
42 |
Option(URLConnection.guessContentTypeFromName(file_name)).getOrElse(default_mime_type) |
|
43 |
Content(bytes, file_name = file_name, mime_type = mime_type) |
|
44 |
} |
|
45 |
||
46 |
def read_content(path: Path): Content = read_content(path.file) |
|
47 |
||
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
48 |
|
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
49 |
|
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
50 |
/** client **/ |
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
51 |
|
| 73417 | 52 |
val NEWLINE: String = "\r\n" |
53 |
||
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
54 |
object Client |
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
55 |
{
|
| 73439 | 56 |
val default_timeout: Time = Time.seconds(180) |
| 73422 | 57 |
|
58 |
def open_connection(url: URL, |
|
59 |
timeout: Time = default_timeout, |
|
60 |
user_agent: String = ""): HttpURLConnection = |
|
| 73421 | 61 |
{
|
62 |
url.openConnection match {
|
|
63 |
case connection: HttpURLConnection => |
|
| 73422 | 64 |
if (0 < timeout.ms && timeout.ms <= Integer.MAX_VALUE) {
|
65 |
val ms = timeout.ms.toInt |
|
66 |
connection.setConnectTimeout(ms) |
|
67 |
connection.setReadTimeout(ms) |
|
68 |
} |
|
| 73421 | 69 |
proper_string(user_agent).foreach(s => connection.setRequestProperty("User-Agent", s))
|
70 |
connection |
|
71 |
case _ => error("Bad URL (not HTTP): " + quote(url.toString))
|
|
72 |
} |
|
73 |
} |
|
| 73417 | 74 |
|
| 73421 | 75 |
def get_content(connection: HttpURLConnection): Content = |
| 73417 | 76 |
{
|
77 |
val Charset = """.*\bcharset="?([\S^"]+)"?.*""".r |
|
|
73429
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
78 |
|
|
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
79 |
val start = Time.now() |
| 73417 | 80 |
using(connection.getInputStream)(stream => |
81 |
{
|
|
82 |
val bytes = Bytes.read_stream(stream, hint = connection.getContentLength) |
|
|
73429
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
83 |
val stop = Time.now() |
|
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
84 |
|
| 73417 | 85 |
val file_name = Url.file_name(connection.getURL) |
86 |
val mime_type = Option(connection.getContentType).getOrElse(default_mime_type) |
|
87 |
val encoding = |
|
88 |
(connection.getContentEncoding, mime_type) match {
|
|
89 |
case (enc, _) if enc != null => enc |
|
90 |
case (_, Charset(enc)) => enc |
|
91 |
case _ => default_encoding |
|
92 |
} |
|
|
73429
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
93 |
Content(bytes, file_name = file_name, mime_type = mime_type, |
|
8970081c6500
elapsed time to download content (and for the server to provide content);
wenzelm
parents:
73425
diff
changeset
|
94 |
encoding = encoding, elapsed_time = stop - start) |
| 73417 | 95 |
}) |
96 |
} |
|
97 |
||
| 73422 | 98 |
def get(url: URL, timeout: Time = default_timeout, user_agent: String = ""): Content = |
99 |
get_content(open_connection(url, timeout = timeout, user_agent = user_agent)) |
|
| 73417 | 100 |
|
| 73422 | 101 |
def post(url: URL, parameters: List[(String, Any)], |
102 |
timeout: Time = default_timeout, |
|
103 |
user_agent: String = ""): Content = |
|
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
104 |
{
|
| 73422 | 105 |
val connection = open_connection(url, timeout = timeout, user_agent = user_agent) |
| 73421 | 106 |
connection.setRequestMethod("POST")
|
107 |
connection.setDoOutput(true) |
|
| 73417 | 108 |
|
| 74094 | 109 |
val boundary = UUID.random().toString |
| 73421 | 110 |
connection.setRequestProperty( |
111 |
"Content-Type", "multipart/form-data; boundary=" + quote(boundary)) |
|
| 73417 | 112 |
|
| 73421 | 113 |
using(connection.getOutputStream)(out => |
114 |
{
|
|
115 |
def output(s: String): Unit = out.write(UTF8.bytes(s)) |
|
116 |
def output_newline(n: Int = 1): Unit = (1 to n).foreach(_ => output(NEWLINE)) |
|
117 |
def output_boundary(end: Boolean = false): Unit = |
|
118 |
output("--" + boundary + (if (end) "--" else "") + NEWLINE)
|
|
119 |
def output_name(name: String): Unit = |
|
120 |
output("Content-Disposition: form-data; name=" + quote(name))
|
|
121 |
def output_value(value: Any): Unit = |
|
122 |
{
|
|
123 |
output_newline(2) |
|
124 |
output(value.toString) |
|
125 |
} |
|
126 |
def output_content(content: Content): Unit = |
|
127 |
{
|
|
128 |
proper_string(content.file_name).foreach(s => output("; filename=" + quote(s)))
|
|
129 |
output_newline() |
|
130 |
proper_string(content.mime_type).foreach(s => output("Content-Type: " + s))
|
|
131 |
output_newline(2) |
|
132 |
content.bytes.write_stream(out) |
|
133 |
} |
|
| 73417 | 134 |
|
| 73421 | 135 |
output_newline(2) |
| 73417 | 136 |
|
| 73421 | 137 |
for { (name, value) <- parameters } {
|
138 |
output_boundary() |
|
139 |
output_name(name) |
|
140 |
value match {
|
|
141 |
case content: Content => output_content(content) |
|
142 |
case file: JFile => output_content(read_content(file)) |
|
143 |
case path: Path => output_content(read_content(path)) |
|
144 |
case _ => output_value(value) |
|
145 |
} |
|
146 |
output_newline() |
|
147 |
} |
|
148 |
output_boundary(end = true) |
|
149 |
out.flush() |
|
150 |
}) |
|
| 73417 | 151 |
|
| 73421 | 152 |
get_content(connection) |
|
73416
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
153 |
} |
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
154 |
} |
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
155 |
|
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
156 |
|
|
08aa4c1ed579
clarified signature: more explicit HTTP operations;
wenzelm
parents:
73413
diff
changeset
|
157 |
|
| 65081 | 158 |
/** server **/ |
159 |
||
| 65078 | 160 |
/* response */ |
161 |
||
162 |
object Response |
|
163 |
{
|
|
164 |
def apply( |
|
165 |
bytes: Bytes = Bytes.empty, |
|
| 73417 | 166 |
content_type: String = mime_type_bytes): Response = |
| 65078 | 167 |
new Response(bytes, content_type) |
168 |
||
169 |
val empty: Response = apply() |
|
| 73417 | 170 |
def text(s: String): Response = apply(Bytes(s), mime_type_text) |
171 |
def html(s: String): Response = apply(Bytes(s), mime_type_html) |
|
| 65078 | 172 |
} |
173 |
||
174 |
class Response private[HTTP](val bytes: Bytes, val content_type: String) |
|
175 |
{
|
|
176 |
override def toString: String = bytes.toString |
|
177 |
} |
|
178 |
||
179 |
||
180 |
/* exchange */ |
|
181 |
||
182 |
class Exchange private[HTTP](val http_exchange: HttpExchange) |
|
183 |
{
|
|
184 |
def request_method: String = http_exchange.getRequestMethod |
|
185 |
def request_uri: URI = http_exchange.getRequestURI |
|
186 |
||
187 |
def read_request(): Bytes = |
|
|
69393
ed0824ef337e
static type for Library.using: avoid Java 11 warnings on "Illegal reflective access";
wenzelm
parents:
69374
diff
changeset
|
188 |
using(http_exchange.getRequestBody)(Bytes.read_stream(_)) |
| 65078 | 189 |
|
| 73340 | 190 |
def write_response(code: Int, response: Response): Unit = |
| 65078 | 191 |
{
|
| 73439 | 192 |
http_exchange.getResponseHeaders.set("Content-Type", response.content_type)
|
| 65078 | 193 |
http_exchange.sendResponseHeaders(code, response.bytes.length.toLong) |
| 73439 | 194 |
using(http_exchange.getResponseBody)(response.bytes.write_stream) |
| 65078 | 195 |
} |
196 |
} |
|
197 |
||
198 |
||
| 73413 | 199 |
/* handler for request method */ |
| 66207 | 200 |
|
201 |
sealed case class Arg(method: String, uri: URI, request: Bytes) |
|
202 |
{
|
|
203 |
def decode_properties: Properties.T = |
|
|
73716
00ef0f401a29
more uniform use of Properties.Eq.unapply, with slightly changed semantics in boundary cases;
wenzelm
parents:
73547
diff
changeset
|
204 |
space_explode('&', request.text).map(
|
|
00ef0f401a29
more uniform use of Properties.Eq.unapply, with slightly changed semantics in boundary cases;
wenzelm
parents:
73547
diff
changeset
|
205 |
{
|
|
00ef0f401a29
more uniform use of Properties.Eq.unapply, with slightly changed semantics in boundary cases;
wenzelm
parents:
73547
diff
changeset
|
206 |
case Properties.Eq(a, b) => Url.decode(a) -> Url.decode(b) |
|
00ef0f401a29
more uniform use of Properties.Eq.unapply, with slightly changed semantics in boundary cases;
wenzelm
parents:
73547
diff
changeset
|
207 |
case s => error("Malformed key-value pair in HTTP/POST: " + quote(s))
|
| 66207 | 208 |
}) |
209 |
} |
|
210 |
||
| 73413 | 211 |
object Handler |
212 |
{
|
|
213 |
def apply(root: String, body: Exchange => Unit): Handler = |
|
| 73439 | 214 |
new Handler(root, (x: HttpExchange) => body(new Exchange(x))) |
| 73413 | 215 |
|
216 |
def method(name: String, root: String, body: Arg => Option[Response]): Handler = |
|
217 |
apply(root, http => |
|
218 |
{
|
|
219 |
val request = http.read_request() |
|
220 |
if (http.request_method == name) {
|
|
221 |
val arg = Arg(name, http.request_uri, request) |
|
222 |
Exn.capture(body(arg)) match {
|
|
223 |
case Exn.Res(Some(response)) => |
|
224 |
http.write_response(200, response) |
|
225 |
case Exn.Res(None) => |
|
226 |
http.write_response(404, Response.empty) |
|
227 |
case Exn.Exn(ERROR(msg)) => |
|
228 |
http.write_response(500, Response.text(Output.error_message_text(msg))) |
|
229 |
case Exn.Exn(exn) => throw exn |
|
230 |
} |
|
| 65078 | 231 |
} |
| 73413 | 232 |
else http.write_response(400, Response.empty) |
233 |
}) |
|
| 65076 | 234 |
|
| 73413 | 235 |
def get(root: String, body: Arg => Option[Response]): Handler = method("GET", root, body)
|
236 |
def post(root: String, body: Arg => Option[Response]): Handler = method("POST", root, body)
|
|
237 |
} |
|
238 |
||
239 |
class Handler private(val root: String, val handler: HttpHandler) |
|
240 |
{
|
|
241 |
override def toString: String = root |
|
242 |
} |
|
| 66207 | 243 |
|
| 65077 | 244 |
|
245 |
/* server */ |
|
246 |
||
247 |
class Server private[HTTP](val http_server: HttpServer) |
|
248 |
{
|
|
| 73340 | 249 |
def += (handler: Handler): Unit = http_server.createContext(handler.root, handler.handler) |
250 |
def -= (handler: Handler): Unit = http_server.removeContext(handler.root) |
|
| 65077 | 251 |
|
| 73439 | 252 |
def start(): Unit = http_server.start() |
| 73367 | 253 |
def stop(): Unit = http_server.stop(0) |
| 65077 | 254 |
|
255 |
def address: InetSocketAddress = http_server.getAddress |
|
256 |
def url: String = "http://" + address.getHostName + ":" + address.getPort |
|
257 |
override def toString: String = url |
|
258 |
} |
|
259 |
||
| 65081 | 260 |
def server(handlers: List[Handler] = isabelle_resources): Server = |
| 63823 | 261 |
{
|
| 69463 | 262 |
val http_server = HttpServer.create(new InetSocketAddress(isabelle.Server.localhost, 0), 0) |
| 65077 | 263 |
http_server.setExecutor(null) |
| 65076 | 264 |
|
| 65077 | 265 |
val server = new Server(http_server) |
266 |
for (handler <- handlers) server += handler |
|
267 |
server |
|
| 63823 | 268 |
} |
| 65079 | 269 |
|
270 |
||
| 65081 | 271 |
|
272 |
/** Isabelle resources **/ |
|
273 |
||
274 |
lazy val isabelle_resources: List[Handler] = |
|
| 73518 | 275 |
List(welcome(), fonts()) |
| 65081 | 276 |
|
277 |
||
278 |
/* welcome */ |
|
279 |
||
| 73518 | 280 |
def welcome(root: String = "/"): Handler = |
281 |
Handler.get(root, arg => |
|
282 |
if (arg.uri.toString == root) {
|
|
| 73547 | 283 |
Some(Response.text("Welcome to " + Isabelle_System.identification()))
|
| 65081 | 284 |
} |
285 |
else None) |
|
286 |
||
287 |
||
288 |
/* fonts */ |
|
| 65079 | 289 |
|
| 69374 | 290 |
private lazy val html_fonts: List[Isabelle_Fonts.Entry] = |
291 |
Isabelle_Fonts.fonts(hidden = true) |
|
| 65079 | 292 |
|
293 |
def fonts(root: String = "/fonts"): Handler = |
|
| 73413 | 294 |
Handler.get(root, arg => |
| 65079 | 295 |
{
|
| 66479 | 296 |
val uri_name = arg.uri.toString |
|
69364
91dbade9a5fa
proper font file name for HTTP (amending dc9a39c3f75d);
wenzelm
parents:
69363
diff
changeset
|
297 |
if (uri_name == root) {
|
| 69366 | 298 |
Some(Response.text(cat_lines(html_fonts.map(entry => entry.path.file_name)))) |
|
69364
91dbade9a5fa
proper font file name for HTTP (amending dc9a39c3f75d);
wenzelm
parents:
69363
diff
changeset
|
299 |
} |
|
91dbade9a5fa
proper font file name for HTTP (amending dc9a39c3f75d);
wenzelm
parents:
69363
diff
changeset
|
300 |
else {
|
|
91dbade9a5fa
proper font file name for HTTP (amending dc9a39c3f75d);
wenzelm
parents:
69363
diff
changeset
|
301 |
html_fonts.collectFirst( |
| 69366 | 302 |
{ case entry if uri_name == root + "/" + entry.path.file_name => Response(entry.bytes) })
|
|
69364
91dbade9a5fa
proper font file name for HTTP (amending dc9a39c3f75d);
wenzelm
parents:
69363
diff
changeset
|
303 |
} |
| 65079 | 304 |
}) |
| 63823 | 305 |
} |