blob: 74d5d1d0e82d99876dd774d1eb82aac449a3128f [file] [log] [blame] [raw]
Florian Nücke03ed6172014-01-19 18:13:30 +01001package li.cil.oc.server.component
2
Florian Nückefa5a6f12015-03-27 19:25:39 +01003import java.io.{BufferedWriter, FileNotFoundException, IOException, InputStream, OutputStreamWriter}
Florian Nücke959e5672014-01-20 19:27:45 +01004import java.net._
5import java.nio.ByteBuffer
6import java.nio.channels.SocketChannel
Florian Nückefa5a6f12015-03-27 19:25:39 +01007import java.util.concurrent.{Callable, ConcurrentLinkedQueue, ExecutionException, Future}
Florian Nücke66e7f252014-06-19 15:19:13 +02008
Florian Nückefa5a6f12015-03-27 19:25:39 +01009import li.cil.oc.{OpenComputers, Settings, api}
10import li.cil.oc.api.machine.{Arguments, Callback, Context}
Florian Nücke03ed6172014-01-19 18:13:30 +010011import li.cil.oc.api.network._
Florian Nückefa5a6f12015-03-27 19:25:39 +010012import li.cil.oc.api.{Network, prefab}
Florian Nücke34083e92015-02-05 12:40:31 +010013import li.cil.oc.api.prefab.AbstractValue
Florian Nückeb12913b2014-02-08 22:09:33 +010014import li.cil.oc.util.ExtendedNBT._
Florian Nücke03ed6172014-01-19 18:13:30 +010015import li.cil.oc.util.ThreadPoolFactory
Florian Nücke03ed6172014-01-19 18:13:30 +010016import net.minecraft.nbt.NBTTagCompound
17import net.minecraft.server.MinecraftServer
Florian Nücke66e7f252014-06-19 15:19:13 +020018
Florian Nückeb12913b2014-02-08 22:09:33 +010019import scala.collection.mutable
Florian Nücke03ed6172014-01-19 18:13:30 +010020
Florian Nücke3841fb42014-10-05 16:19:19 +020021class InternetCard extends prefab.ManagedEnvironment {
22 override val node = Network.newNode(this, Visibility.Network).
Florian Nücke03ed6172014-01-19 18:13:30 +010023 withComponent("internet", Visibility.Neighbors).
24 create()
Florian Nücke190d8e12014-02-02 13:49:45 +010025
26 val romInternet = Option(api.FileSystem.asManagedEnvironment(api.FileSystem.
27 fromClass(OpenComputers.getClass, Settings.resourceDomain, "lua/component/internet"), "internet"))
Florian Nückeb12913b2014-02-08 22:09:33 +010028
29 protected var owner: Option[Context] = None
Florian Nücke03ed6172014-01-19 18:13:30 +010030
Florian Nücke34083e92015-02-05 12:40:31 +010031 protected val connections = mutable.Set.empty[InternetCard.Closable]
Florian Nücke03ed6172014-01-19 18:13:30 +010032
33 // ----------------------------------------------------------------------- //
34
Florian Nückec4da02b2014-02-11 23:35:46 +010035 @Callback(direct = true, doc = """function():boolean -- Returns whether HTTP requests can be made (config setting).""")
Florian Nücke03ed6172014-01-19 18:13:30 +010036 def isHttpEnabled(context: Context, args: Arguments): Array[AnyRef] = result(Settings.get.httpEnabled)
37
Florian Nückeef6d2512015-03-27 15:52:22 +010038 @Callback(doc = """function(url:string[, postData:string]):userdata -- Starts an HTTP request. If this returns true, further results will be pushed using `http_response` signals.""")
Florian Nücke9b154782014-06-04 18:55:41 +020039 def request(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
Florian Nückebff2da72014-05-20 18:07:15 +020040 checkOwner(context)
Florian Nücke03ed6172014-01-19 18:13:30 +010041 val address = args.checkString(0)
Florian Nücke94c3cc22014-01-26 15:39:06 +010042 if (!Settings.get.httpEnabled) {
Florian Nückeb931bae2014-02-12 14:51:52 +010043 return result(Unit, "http requests are unavailable")
Florian Nücke94c3cc22014-01-26 15:39:06 +010044 }
Florian Nücke9b154782014-06-04 18:55:41 +020045 if (connections.size >= Settings.get.maxConnections) {
46 throw new IOException("too many open connections")
Florian Nücke94c3cc22014-01-26 15:39:06 +010047 }
Florian Nücke9b154782014-06-04 18:55:41 +020048 val post = if (args.isString(1)) Option(args.checkString(1)) else None
Florian Nücke34083e92015-02-05 12:40:31 +010049 val request = new InternetCard.HTTPRequest(this, checkAddress(address), post)
50 connections += request
51 result(request)
Florian Nücke03ed6172014-01-19 18:13:30 +010052 }
53
Florian Nückec4da02b2014-02-11 23:35:46 +010054 @Callback(direct = true, doc = """function():boolean -- Returns whether TCP connections can be made (config setting).""")
Vexatos4199f122014-12-02 21:18:13 +010055 def isTcpEnabled(context: Context, args: Arguments): Array[AnyRef] = result(Settings.get.tcpEnabled)
Florian Nücke959e5672014-01-20 19:27:45 +010056
Florian Nückeef6d2512015-03-27 15:52:22 +010057 @Callback(doc = """function(address:string[, port:number]):userdata -- Opens a new TCP connection. Returns the handle of the connection.""")
Florian Nücke05f6fcf2014-02-24 11:17:05 +010058 def connect(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
Florian Nückebff2da72014-05-20 18:07:15 +020059 checkOwner(context)
Florian Nücke959e5672014-01-20 19:27:45 +010060 val address = args.checkString(0)
Florian Nückef61d1592014-10-17 19:00:57 +020061 val port = args.optInteger(1, -1)
Florian Nückeb931bae2014-02-12 14:51:52 +010062 if (!Settings.get.tcpEnabled) {
63 return result(Unit, "tcp connections are unavailable")
64 }
Florian Nücke959e5672014-01-20 19:27:45 +010065 if (connections.size >= Settings.get.maxConnections) {
66 throw new IOException("too many open connections")
Florian Nücke03ed6172014-01-19 18:13:30 +010067 }
Florian Nücke4f2b9422014-09-04 16:47:47 +020068 val uri = checkUri(address, port)
Florian Nücke34083e92015-02-05 12:40:31 +010069 val socket = new InternetCard.TCPSocket(this, uri, port)
70 connections += socket
71 result(socket)
Florian Nücke03ed6172014-01-19 18:13:30 +010072 }
73
Florian Nückebff2da72014-05-20 18:07:15 +020074 private def checkOwner(context: Context) {
75 if (owner.isEmpty || context.node != owner.get.node) {
76 throw new IllegalArgumentException("can only be used by the owning computer")
77 }
78 }
79
Florian Nücke03ed6172014-01-19 18:13:30 +010080 // ----------------------------------------------------------------------- //
81
Florian Nücke190d8e12014-02-02 13:49:45 +010082 override def onConnect(node: Node) {
83 super.onConnect(node)
Florian Nücke38c231e2014-02-10 18:16:16 +010084 if (owner.isEmpty && node.host.isInstanceOf[Context] && node.isNeighborOf(this.node)) {
Florian Nückeb12913b2014-02-08 22:09:33 +010085 owner = Some(node.host.asInstanceOf[Context])
Florian Nücke8c0c8082014-02-28 19:32:53 +010086 romInternet.foreach(fs => node.connect(fs.node))
Florian Nücke190d8e12014-02-02 13:49:45 +010087 }
88 }
89
Florian Nücke05f6fcf2014-02-24 11:17:05 +010090 override def onDisconnect(node: Node) = this.synchronized {
Florian Nücke94c3cc22014-01-26 15:39:06 +010091 super.onDisconnect(node)
Florian Nücke38c231e2014-02-10 18:16:16 +010092 if (owner.isDefined && (node == this.node || node.host.isInstanceOf[Context] && (node.host.asInstanceOf[Context] == owner.get))) {
Florian Nückeb12913b2014-02-08 22:09:33 +010093 owner = None
Florian Nücke9b154782014-06-04 18:55:41 +020094 this.synchronized {
Florian Nücke34083e92015-02-05 12:40:31 +010095 connections.foreach(_.close())
Florian Nücke9b154782014-06-04 18:55:41 +020096 connections.clear()
Florian Nücke94c3cc22014-01-26 15:39:06 +010097 }
Florian Nücke190d8e12014-02-02 13:49:45 +010098 romInternet.foreach(_.node.remove())
Florian Nücke94c3cc22014-01-26 15:39:06 +010099 }
100 }
Florian Nücke959e5672014-01-20 19:27:45 +0100101
Florian Nücke05f6fcf2014-02-24 11:17:05 +0100102 override def onMessage(message: Message) = this.synchronized {
Florian Nücke959e5672014-01-20 19:27:45 +0100103 super.onMessage(message)
104 message.data match {
Florian Nückeb12913b2014-02-08 22:09:33 +0100105 case Array() if (message.name == "computer.stopped" || message.name == "computer.started") && owner.isDefined && message.source.address == owner.get.node.address =>
Florian Nücke1694c102014-04-28 20:27:53 +0200106 this.synchronized {
Florian Nücke34083e92015-02-05 12:40:31 +0100107 connections.foreach(_.close())
Florian Nücke9b154782014-06-04 18:55:41 +0200108 connections.clear()
Florian Nücke94c3cc22014-01-26 15:39:06 +0100109 }
Florian Nücke959e5672014-01-20 19:27:45 +0100110 case _ =>
Florian Nücke03ed6172014-01-19 18:13:30 +0100111 }
112 }
113
114 // ----------------------------------------------------------------------- //
115
116 override def load(nbt: NBTTagCompound) {
117 super.load(nbt)
Florian Nücke52f234d2014-02-02 20:18:47 +0100118 romInternet.foreach(_.load(nbt.getCompoundTag("romInternet")))
Florian Nücke03ed6172014-01-19 18:13:30 +0100119 }
120
121 override def save(nbt: NBTTagCompound) {
122 super.save(nbt)
Florian Nücke8c0c8082014-02-28 19:32:53 +0100123 romInternet.foreach(fs => nbt.setNewCompoundTag("romInternet", fs.save))
Florian Nücke03ed6172014-01-19 18:13:30 +0100124 }
Florian Nücke959e5672014-01-20 19:27:45 +0100125
126 // ----------------------------------------------------------------------- //
127
Florian Nücke4f2b9422014-09-04 16:47:47 +0200128 private def checkUri(address: String, port: Int): URI = {
Florian Nückefb4d28b2014-01-26 19:26:26 +0100129 try {
130 val parsed = new URI(address)
Florian Nücke4f2b9422014-09-04 16:47:47 +0200131 if (parsed.getHost != null && (parsed.getPort > 0 || port > 0)) {
Florian Nückefb4d28b2014-01-26 19:26:26 +0100132 return parsed
Florian Nücke959e5672014-01-20 19:27:45 +0100133 }
Florian Nücke959e5672014-01-20 19:27:45 +0100134 }
Florian Nückefb4d28b2014-01-26 19:26:26 +0100135 catch {
136 case _: Throwable =>
137 }
138
139 val simple = new URI("oc://" + address)
Florian Nücke4f2b9422014-09-04 16:47:47 +0200140 if (simple.getHost != null) {
141 if (simple.getPort > 0)
142 return simple
143 else if (port > 0)
144 return new URI(simple.toString + ":" + port)
Florian Nückefb4d28b2014-01-26 19:26:26 +0100145 }
146
Florian Nücke4f2b9422014-09-04 16:47:47 +0200147 throw new IllegalArgumentException("address could not be parsed or no valid port given")
Florian Nücke959e5672014-01-20 19:27:45 +0100148 }
149
150 private def checkAddress(address: String) = {
151 val url = try new URL(address)
152 catch {
153 case e: Throwable => throw new FileNotFoundException("invalid address")
154 }
155 val protocol = url.getProtocol
156 if (!protocol.matches("^https?$")) {
157 throw new FileNotFoundException("unsupported protocol")
158 }
Florian Nücke959e5672014-01-20 19:27:45 +0100159 url
Florian Nücke959e5672014-01-20 19:27:45 +0100160 }
Florian Nücke03ed6172014-01-19 18:13:30 +0100161}
162
163object InternetCard {
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200164 private val threadPool = ThreadPoolFactory.create("Internet", Settings.get.internetThreads)
165
Florian Nücke34083e92015-02-05 12:40:31 +0100166 trait Closable {
167 def close(): Unit
168 }
169
170 class TCPSocket extends AbstractValue with Closable {
171 def this(owner: InternetCard, uri: URI, port: Int) {
172 this()
173 this.owner = Some(owner)
174 channel = SocketChannel.open()
175 channel.configureBlocking(false)
176 address = threadPool.submit(new AddressResolver(uri, port))
177 }
178
179 private var owner: Option[InternetCard] = None
180 private var address: Future[InetAddress] = null
181 private var channel: SocketChannel = null
182 private var isAddressResolved = false
183
184 @Callback(doc = """function():boolean -- Ensures a socket is connected. Errors if the connection failed.""")
185 def finishConnect(context: Context, args: Arguments): Array[AnyRef] = this.synchronized(result(checkConnected()))
186
187 @Callback(doc = """function([n:number]):string -- Tries to read data from the socket stream. Returns the read byte array.""")
188 def read(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
189 val n = math.min(Settings.get.maxReadBuffer, math.max(0, args.optInteger(1, Int.MaxValue)))
190 if (checkConnected()) {
191 val buffer = ByteBuffer.allocate(n)
192 val read = channel.read(buffer)
193 if (read == -1) result(null)
194 else result(buffer.array.view(0, read).toArray)
195 }
196 else result(Array.empty[Byte])
197 }
198
199 @Callback(doc = """function(data:string):number -- Tries to write data to the socket stream. Returns the number of bytes written.""")
200 def write(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
201 if (checkConnected()) {
202 val value = args.checkByteArray(0)
203 result(channel.write(ByteBuffer.wrap(value)))
204 }
205 else result(0)
206 }
207
208 @Callback(direct = true, doc = """function() -- Closes an open socket stream.""")
209 def close(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
210 close()
211 null
212 }
213
214 override def dispose(context: Context): Unit = {
215 super.dispose(context)
216 close()
217 }
218
219 override def close(): Unit = {
220 owner.foreach(card => {
221 card.connections.remove(this)
222 address.cancel(true)
223 channel.close()
224 owner = None
225 address = null
226 channel = null
227 })
228 }
229
Florian Nückefa5a6f12015-03-27 19:25:39 +0100230 private def checkConnected() = {
Florian Nücke34083e92015-02-05 12:40:31 +0100231 if (owner.isEmpty) throw new IOException("connection lost")
Florian Nückefa5a6f12015-03-27 19:25:39 +0100232 try {
233 if (isAddressResolved) channel.finishConnect()
234 else if (address.isCancelled) {
235 // I don't think this can ever happen, Justin Case.
236 channel.close()
237 throw new IOException("bad connection descriptor")
Florian Nücke34083e92015-02-05 12:40:31 +0100238 }
Florian Nückefa5a6f12015-03-27 19:25:39 +0100239 else if (address.isDone) {
240 // Check for errors.
241 try address.get catch {
242 case e: ExecutionException => throw e.getCause
243 }
244 isAddressResolved = true
245 false
246 }
247 else false
Florian Nücke34083e92015-02-05 12:40:31 +0100248 }
Florian Nückefa5a6f12015-03-27 19:25:39 +0100249 catch {
250 case t: Throwable =>
251 close()
252 false
253 }
Florian Nücke34083e92015-02-05 12:40:31 +0100254 }
255
256 // This has to be an explicit internal class instead of an anonymous one
257 // because the scala compiler breaks otherwise. Yay for compiler bugs.
258 private class AddressResolver(val uri: URI, val port: Int) extends Callable[InetAddress] {
259 override def call(): InetAddress = {
260 val resolved = InetAddress.getByName(uri.getHost)
261 checkLists(resolved, uri.getHost)
262 val address = new InetSocketAddress(resolved, if (uri.getPort != -1) uri.getPort else port)
263 channel.connect(address)
264 resolved
265 }
266 }
267 }
268
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200269 def checkLists(inetAddress: InetAddress, host: String) {
270 if (Settings.get.httpHostWhitelist.length > 0 && !Settings.get.httpHostWhitelist.exists(_(inetAddress, host))) {
271 throw new FileNotFoundException("address is not whitelisted")
272 }
Florian Nückecb69a4b2014-06-04 12:58:26 +0200273 if (Settings.get.httpHostBlacklist.length > 0 && Settings.get.httpHostBlacklist.exists(_(inetAddress, host))) {
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200274 throw new FileNotFoundException("address is blacklisted")
275 }
276 }
277
Florian Nücke34083e92015-02-05 12:40:31 +0100278 class HTTPRequest extends AbstractValue with Closable {
279 def this(owner: InternetCard, url: URL, post: Option[String]) {
280 this()
281 this.owner = Some(owner)
282 this.stream = threadPool.submit(new RequestSender(url, post))
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200283 }
284
Florian Nücke34083e92015-02-05 12:40:31 +0100285 private var owner: Option[InternetCard] = None
286 private var response: Option[(Int, String, AnyRef)] = None
287 private var stream: Future[InputStream] = null
288 private val queue = new ConcurrentLinkedQueue[Byte]()
289 private var reader: Future[_] = null
290 private var eof = false
291
292 @Callback(doc = """function():boolean -- Ensures a response is available. Errors if the connection failed.""")
293 def finishConnect(context: Context, args: Arguments): Array[AnyRef] = this.synchronized(result(checkResponse()))
294
295 @Callback(direct = true, doc = """function():number, string, table -- Get response code, message and headers.""")
296 def response(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
297 response match {
298 case Some((code, message, headers)) => result(code, message, headers)
299 case _ => result(null)
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200300 }
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200301 }
302
Florian Nücke34083e92015-02-05 12:40:31 +0100303 @Callback(doc = """function([n:number]):string -- Tries to read data from the response. Returns the read byte array.""")
304 def read(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
305 val n = math.min(Settings.get.maxReadBuffer, math.max(0, args.optInteger(1, Int.MaxValue)))
306 if (checkResponse()) {
307 if (eof && queue.isEmpty) result(null)
308 else {
309 val buffer = ByteBuffer.allocate(n)
310 var read = 0
311 while (!queue.isEmpty && read < n) {
312 buffer.put(queue.poll())
313 read += 1
314 }
315 if (read == 0) {
316 readMore()
317 }
318 result(buffer.array.view(0, read).toArray)
319 }
320 }
321 else result(Array.empty[Byte])
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200322 }
323
Florian Nücke34083e92015-02-05 12:40:31 +0100324 @Callback(direct = true, doc = """function() -- Closes an open socket stream.""")
325 def close(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
326 close()
327 null
328 }
329
330 override def dispose(context: Context): Unit = {
331 super.dispose(context)
332 close()
333 }
334
335 override def close(): Unit = {
336 owner.foreach(card => {
337 card.connections.remove(this)
338 stream.cancel(true)
339 if (reader != null) {
340 reader.cancel(true)
341 }
342 owner = None
343 stream = null
344 reader = null
345 })
346 }
347
348 private def checkResponse() = this.synchronized {
349 if (owner.isEmpty) throw new IOException("connection lost")
350 if (stream.isDone) {
351 if (reader == null) {
352 // Check for errors.
353 try stream.get catch {
354 case e: ExecutionException => throw e.getCause
355 }
356 readMore()
357 }
358 true
Florian Nücke4d3c0a02014-05-24 20:12:14 +0200359 }
360 else false
361 }
Florian Nücke1694c102014-04-28 20:27:53 +0200362
Florian Nücke34083e92015-02-05 12:40:31 +0100363 private def readMore(): Unit = {
364 if (reader == null || reader.isCancelled || reader.isDone) {
365 if (!eof) reader = threadPool.submit(new Runnable {
366 override def run(): Unit = {
367 val buffer = new Array[Byte](Settings.get.maxReadBuffer)
368 val count = stream.get.read(buffer)
369 if (count < 0) {
370 eof = true
Florian Nücke9b154782014-06-04 18:55:41 +0200371 }
Florian Nücke34083e92015-02-05 12:40:31 +0100372 for (i <- 0 until count) {
373 queue.add(buffer(i))
374 }
Florian Nücke9b154782014-06-04 18:55:41 +0200375 }
Florian Nücke34083e92015-02-05 12:40:31 +0100376 })
Florian Nücke9b154782014-06-04 18:55:41 +0200377 }
378 }
Florian Nücke34083e92015-02-05 12:40:31 +0100379
380 // This one doesn't (see comment in TCP socket), but I like to keep it consistent.
381 private class RequestSender(val url: URL, val post: Option[String]) extends Callable[InputStream] {
382 override def call() = try {
383 checkLists(InetAddress.getByName(url.getHost), url.getHost)
384 val proxy = Option(MinecraftServer.getServer.getServerProxy).getOrElse(java.net.Proxy.NO_PROXY)
385 url.openConnection(proxy) match {
386 case http: HttpURLConnection => try {
387 http.setDoInput(true)
388 if (post.isDefined) {
389 http.setRequestMethod("POST")
390 http.setDoOutput(true)
391 http.setReadTimeout(Settings.get.httpTimeout)
392
393 val out = new BufferedWriter(new OutputStreamWriter(http.getOutputStream))
394 out.write(post.get)
395 out.close()
396 }
397 else {
398 http.setRequestMethod("GET")
399 http.setDoOutput(false)
400 }
401
402 val input = http.getInputStream
403 HTTPRequest.this.synchronized {
404 response = Some((http.getResponseCode, http.getResponseMessage, http.getHeaderFields))
405 }
406 input
407 }
408 catch {
409 case t: Throwable =>
410 http.disconnect()
411 throw t
412 }
413 case other => throw new IOException("unexpected connection type")
414 }
415 }
416 catch {
417 case e: UnknownHostException =>
418 throw new IOException("unknown host: " + Option(e.getMessage).getOrElse(e.toString))
419 case e: Throwable =>
420 throw new IOException(Option(e.getMessage).getOrElse(e.toString))
421 }
Florian Nücke1694c102014-04-28 20:27:53 +0200422 }
Florian Nückefa5a6f12015-03-27 19:25:39 +0100423
Florian Nücke1694c102014-04-28 20:27:53 +0200424 }
425
Vexatos4199f122014-12-02 21:18:13 +0100426}