Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 1 | package li.cil.oc.server.component |
| 2 | |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 3 | import java.io.{BufferedWriter, FileNotFoundException, IOException, InputStream, OutputStreamWriter} |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 4 | import java.net._ |
| 5 | import java.nio.ByteBuffer |
| 6 | import java.nio.channels.SocketChannel |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 7 | import java.util.concurrent.{Callable, ConcurrentLinkedQueue, ExecutionException, Future} |
Florian Nücke | 66e7f25 | 2014-06-19 15:19:13 +0200 | [diff] [blame] | 8 | |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 9 | import li.cil.oc.{OpenComputers, Settings, api} |
| 10 | import li.cil.oc.api.machine.{Arguments, Callback, Context} |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 11 | import li.cil.oc.api.network._ |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 12 | import li.cil.oc.api.{Network, prefab} |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 13 | import li.cil.oc.api.prefab.AbstractValue |
Florian Nücke | b12913b | 2014-02-08 22:09:33 +0100 | [diff] [blame] | 14 | import li.cil.oc.util.ExtendedNBT._ |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 15 | import li.cil.oc.util.ThreadPoolFactory |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 16 | import net.minecraft.nbt.NBTTagCompound |
| 17 | import net.minecraft.server.MinecraftServer |
Florian Nücke | 66e7f25 | 2014-06-19 15:19:13 +0200 | [diff] [blame] | 18 | |
Florian Nücke | b12913b | 2014-02-08 22:09:33 +0100 | [diff] [blame] | 19 | import scala.collection.mutable |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 20 | |
Florian Nücke | 3841fb4 | 2014-10-05 16:19:19 +0200 | [diff] [blame] | 21 | class InternetCard extends prefab.ManagedEnvironment { |
| 22 | override val node = Network.newNode(this, Visibility.Network). |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 23 | withComponent("internet", Visibility.Neighbors). |
| 24 | create() |
Florian Nücke | 190d8e1 | 2014-02-02 13:49:45 +0100 | [diff] [blame] | 25 | |
| 26 | val romInternet = Option(api.FileSystem.asManagedEnvironment(api.FileSystem. |
| 27 | fromClass(OpenComputers.getClass, Settings.resourceDomain, "lua/component/internet"), "internet")) |
Florian Nücke | b12913b | 2014-02-08 22:09:33 +0100 | [diff] [blame] | 28 | |
| 29 | protected var owner: Option[Context] = None |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 30 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 31 | protected val connections = mutable.Set.empty[InternetCard.Closable] |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 32 | |
| 33 | // ----------------------------------------------------------------------- // |
| 34 | |
Florian Nücke | c4da02b | 2014-02-11 23:35:46 +0100 | [diff] [blame] | 35 | @Callback(direct = true, doc = """function():boolean -- Returns whether HTTP requests can be made (config setting).""") |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 36 | def isHttpEnabled(context: Context, args: Arguments): Array[AnyRef] = result(Settings.get.httpEnabled) |
| 37 | |
Florian Nücke | ef6d251 | 2015-03-27 15:52:22 +0100 | [diff] [blame] | 38 | @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ücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 39 | def request(context: Context, args: Arguments): Array[AnyRef] = this.synchronized { |
Florian Nücke | bff2da7 | 2014-05-20 18:07:15 +0200 | [diff] [blame] | 40 | checkOwner(context) |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 41 | val address = args.checkString(0) |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 42 | if (!Settings.get.httpEnabled) { |
Florian Nücke | b931bae | 2014-02-12 14:51:52 +0100 | [diff] [blame] | 43 | return result(Unit, "http requests are unavailable") |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 44 | } |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 45 | if (connections.size >= Settings.get.maxConnections) { |
| 46 | throw new IOException("too many open connections") |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 47 | } |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 48 | val post = if (args.isString(1)) Option(args.checkString(1)) else None |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 49 | val request = new InternetCard.HTTPRequest(this, checkAddress(address), post) |
| 50 | connections += request |
| 51 | result(request) |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 52 | } |
| 53 | |
Florian Nücke | c4da02b | 2014-02-11 23:35:46 +0100 | [diff] [blame] | 54 | @Callback(direct = true, doc = """function():boolean -- Returns whether TCP connections can be made (config setting).""") |
Vexatos | 4199f12 | 2014-12-02 21:18:13 +0100 | [diff] [blame] | 55 | def isTcpEnabled(context: Context, args: Arguments): Array[AnyRef] = result(Settings.get.tcpEnabled) |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 56 | |
Florian Nücke | ef6d251 | 2015-03-27 15:52:22 +0100 | [diff] [blame] | 57 | @Callback(doc = """function(address:string[, port:number]):userdata -- Opens a new TCP connection. Returns the handle of the connection.""") |
Florian Nücke | 05f6fcf | 2014-02-24 11:17:05 +0100 | [diff] [blame] | 58 | def connect(context: Context, args: Arguments): Array[AnyRef] = this.synchronized { |
Florian Nücke | bff2da7 | 2014-05-20 18:07:15 +0200 | [diff] [blame] | 59 | checkOwner(context) |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 60 | val address = args.checkString(0) |
Florian Nücke | f61d159 | 2014-10-17 19:00:57 +0200 | [diff] [blame] | 61 | val port = args.optInteger(1, -1) |
Florian Nücke | b931bae | 2014-02-12 14:51:52 +0100 | [diff] [blame] | 62 | if (!Settings.get.tcpEnabled) { |
| 63 | return result(Unit, "tcp connections are unavailable") |
| 64 | } |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 65 | if (connections.size >= Settings.get.maxConnections) { |
| 66 | throw new IOException("too many open connections") |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 67 | } |
Florian Nücke | 4f2b942 | 2014-09-04 16:47:47 +0200 | [diff] [blame] | 68 | val uri = checkUri(address, port) |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 69 | val socket = new InternetCard.TCPSocket(this, uri, port) |
| 70 | connections += socket |
| 71 | result(socket) |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 72 | } |
| 73 | |
Florian Nücke | bff2da7 | 2014-05-20 18:07:15 +0200 | [diff] [blame] | 74 | 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ücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 80 | // ----------------------------------------------------------------------- // |
| 81 | |
Florian Nücke | 190d8e1 | 2014-02-02 13:49:45 +0100 | [diff] [blame] | 82 | override def onConnect(node: Node) { |
| 83 | super.onConnect(node) |
Florian Nücke | 38c231e | 2014-02-10 18:16:16 +0100 | [diff] [blame] | 84 | if (owner.isEmpty && node.host.isInstanceOf[Context] && node.isNeighborOf(this.node)) { |
Florian Nücke | b12913b | 2014-02-08 22:09:33 +0100 | [diff] [blame] | 85 | owner = Some(node.host.asInstanceOf[Context]) |
Florian Nücke | 8c0c808 | 2014-02-28 19:32:53 +0100 | [diff] [blame] | 86 | romInternet.foreach(fs => node.connect(fs.node)) |
Florian Nücke | 190d8e1 | 2014-02-02 13:49:45 +0100 | [diff] [blame] | 87 | } |
| 88 | } |
| 89 | |
Florian Nücke | 05f6fcf | 2014-02-24 11:17:05 +0100 | [diff] [blame] | 90 | override def onDisconnect(node: Node) = this.synchronized { |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 91 | super.onDisconnect(node) |
Florian Nücke | 38c231e | 2014-02-10 18:16:16 +0100 | [diff] [blame] | 92 | if (owner.isDefined && (node == this.node || node.host.isInstanceOf[Context] && (node.host.asInstanceOf[Context] == owner.get))) { |
Florian Nücke | b12913b | 2014-02-08 22:09:33 +0100 | [diff] [blame] | 93 | owner = None |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 94 | this.synchronized { |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 95 | connections.foreach(_.close()) |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 96 | connections.clear() |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 97 | } |
Florian Nücke | 190d8e1 | 2014-02-02 13:49:45 +0100 | [diff] [blame] | 98 | romInternet.foreach(_.node.remove()) |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 99 | } |
| 100 | } |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 101 | |
Florian Nücke | 05f6fcf | 2014-02-24 11:17:05 +0100 | [diff] [blame] | 102 | override def onMessage(message: Message) = this.synchronized { |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 103 | super.onMessage(message) |
| 104 | message.data match { |
Florian Nücke | b12913b | 2014-02-08 22:09:33 +0100 | [diff] [blame] | 105 | case Array() if (message.name == "computer.stopped" || message.name == "computer.started") && owner.isDefined && message.source.address == owner.get.node.address => |
Florian Nücke | 1694c10 | 2014-04-28 20:27:53 +0200 | [diff] [blame] | 106 | this.synchronized { |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 107 | connections.foreach(_.close()) |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 108 | connections.clear() |
Florian Nücke | 94c3cc2 | 2014-01-26 15:39:06 +0100 | [diff] [blame] | 109 | } |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 110 | case _ => |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 111 | } |
| 112 | } |
| 113 | |
| 114 | // ----------------------------------------------------------------------- // |
| 115 | |
| 116 | override def load(nbt: NBTTagCompound) { |
| 117 | super.load(nbt) |
Florian Nücke | 52f234d | 2014-02-02 20:18:47 +0100 | [diff] [blame] | 118 | romInternet.foreach(_.load(nbt.getCompoundTag("romInternet"))) |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 119 | } |
| 120 | |
| 121 | override def save(nbt: NBTTagCompound) { |
| 122 | super.save(nbt) |
Florian Nücke | 8c0c808 | 2014-02-28 19:32:53 +0100 | [diff] [blame] | 123 | romInternet.foreach(fs => nbt.setNewCompoundTag("romInternet", fs.save)) |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 124 | } |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 125 | |
| 126 | // ----------------------------------------------------------------------- // |
| 127 | |
Florian Nücke | 4f2b942 | 2014-09-04 16:47:47 +0200 | [diff] [blame] | 128 | private def checkUri(address: String, port: Int): URI = { |
Florian Nücke | fb4d28b | 2014-01-26 19:26:26 +0100 | [diff] [blame] | 129 | try { |
| 130 | val parsed = new URI(address) |
Florian Nücke | 4f2b942 | 2014-09-04 16:47:47 +0200 | [diff] [blame] | 131 | if (parsed.getHost != null && (parsed.getPort > 0 || port > 0)) { |
Florian Nücke | fb4d28b | 2014-01-26 19:26:26 +0100 | [diff] [blame] | 132 | return parsed |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 133 | } |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 134 | } |
Florian Nücke | fb4d28b | 2014-01-26 19:26:26 +0100 | [diff] [blame] | 135 | catch { |
| 136 | case _: Throwable => |
| 137 | } |
| 138 | |
| 139 | val simple = new URI("oc://" + address) |
Florian Nücke | 4f2b942 | 2014-09-04 16:47:47 +0200 | [diff] [blame] | 140 | 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ücke | fb4d28b | 2014-01-26 19:26:26 +0100 | [diff] [blame] | 145 | } |
| 146 | |
Florian Nücke | 4f2b942 | 2014-09-04 16:47:47 +0200 | [diff] [blame] | 147 | throw new IllegalArgumentException("address could not be parsed or no valid port given") |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 148 | } |
| 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ücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 159 | url |
Florian Nücke | 959e567 | 2014-01-20 19:27:45 +0100 | [diff] [blame] | 160 | } |
Florian Nücke | 03ed617 | 2014-01-19 18:13:30 +0100 | [diff] [blame] | 161 | } |
| 162 | |
| 163 | object InternetCard { |
Florian Nücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 164 | private val threadPool = ThreadPoolFactory.create("Internet", Settings.get.internetThreads) |
| 165 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 166 | 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ücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 230 | private def checkConnected() = { |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 231 | if (owner.isEmpty) throw new IOException("connection lost") |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 232 | 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ücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 238 | } |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 239 | 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ücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 248 | } |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 249 | catch { |
| 250 | case t: Throwable => |
| 251 | close() |
| 252 | false |
| 253 | } |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 254 | } |
| 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ücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 269 | 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ücke | cb69a4b | 2014-06-04 12:58:26 +0200 | [diff] [blame] | 273 | if (Settings.get.httpHostBlacklist.length > 0 && Settings.get.httpHostBlacklist.exists(_(inetAddress, host))) { |
Florian Nücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 274 | throw new FileNotFoundException("address is blacklisted") |
| 275 | } |
| 276 | } |
| 277 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 278 | 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ücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 283 | } |
| 284 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 285 | 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ücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 300 | } |
Florian Nücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 301 | } |
| 302 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 303 | @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ücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 322 | } |
| 323 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 324 | @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ücke | 4d3c0a0 | 2014-05-24 20:12:14 +0200 | [diff] [blame] | 359 | } |
| 360 | else false |
| 361 | } |
Florian Nücke | 1694c10 | 2014-04-28 20:27:53 +0200 | [diff] [blame] | 362 | |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 363 | 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ücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 371 | } |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 372 | for (i <- 0 until count) { |
| 373 | queue.add(buffer(i)) |
| 374 | } |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 375 | } |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 376 | }) |
Florian Nücke | 9b15478 | 2014-06-04 18:55:41 +0200 | [diff] [blame] | 377 | } |
| 378 | } |
Florian Nücke | 34083e9 | 2015-02-05 12:40:31 +0100 | [diff] [blame] | 379 | |
| 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ücke | 1694c10 | 2014-04-28 20:27:53 +0200 | [diff] [blame] | 422 | } |
Florian Nücke | fa5a6f1 | 2015-03-27 19:25:39 +0100 | [diff] [blame] | 423 | |
Florian Nücke | 1694c10 | 2014-04-28 20:27:53 +0200 | [diff] [blame] | 424 | } |
| 425 | |
Vexatos | 4199f12 | 2014-12-02 21:18:13 +0100 | [diff] [blame] | 426 | } |