HTTPプロキシ練習その3

ある程度動くところまでできた。
やな感じにコードが増えつつある。
まだ、画像が途中で切れる(Content-lengthないからか?)などの問題がある。
現状での一番のはまりポイントは、HTTPヘッダのHost:指定だった。
apacheとかの名前ベースVirtualHost(多分)率がわりと高くて
Host:を書いてないとすぐに404とか403になってしまい、何故だか判らずはまってしまった。
せっかくなので、ログ表示を詳しくしてactorとthreadの動きがどうなっているのかを見てみるか。

package proxy 

import io._
import scala.actors._

import java.net.URL
import java.net.ServerSocket
import java.net.Socket

case class Browser(socket :Socket, index :Int)

class Resolver(maxRedirection :Int) extends Actor {

    def act(){
        loop{
            receive {
                case Browser(client, index)=>
                    val iter=Source.fromInputStream(
                            client.getInputStream()).getLines
                    iter.next.split(' ') match {
                        case Array("GET", url, version)=>
                            val c=new HTTPClient(client, index, version)
                            c.get(url)
                        case Array("POST", url, version)=>
                            val c=new HTTPClient(client, index, version)
                            c.post(url)
                        case _ => 0
                    }
                    client.close()
            }
        }
    }
}

class Server(port :Int) {
    def start() {
        val server = new ServerSocket(port)
        println("server: Waiting for connection: "+port)
        var count=0
        def inc(n: Int): Stream[Int]=Stream.cons(n, inc(n+1))
        for(i <- inc(0)){
            val socket=server.accept()
            val resolver= new Resolver(5)
            resolver.start()
            resolver ! Browser(socket, i)
        }
    }
}

object Server {
    def main(args :Array[String]){
        val p=new Server(if(args.length>0){ args(0) toInt } else{ 9998 })
        p.start()
    }
}
package proxy

import collection.mutable.ArrayBuffer
import collection.mutable.Map
import java.net.Socket
import java.net.URL
import java.net.MalformedURLException
import java.io.DataOutputStream
import java.io.IOException
import java.io.DataOutputStream


// http response
case object HTTPResponse{
    val SuccessPattern="""^(\S+)\s+(2\d\d)\s+(.*)""".r
    val RedirectPattern="""^(\S+)\s+(3\d\d)\s+(.*)""".r
    val Pattern="""^(\S+)\s+(\d\d\d)\s+(.*)""".r
    val HeaderPattern="""([^:]+):\s*(.*)""".r
}

class ProxyConnection(url :URL){
    class ProxyRequest(method :String, url :URL, version :String){
        def send(out :DataOutputStream){
            val request=if(url.getQuery==null){
                method+" "+url.getPath+" HTTP/"+version
            }
            else{
                method+" "+url.getPath+"?"+url.getQuery+" HTTP/"+version
            }
            println("proxy request: "+request)
            out.writeBytes(request+"\r\n")
            out.writeBytes("Host: "+url.getHost+"\r\n")
        }
    }

    println("proxy connect: "+url.getHost)
    private val socket=new Socket(url.getHost, if(url.getPort== -1){
            url.getDefaultPort
            }
            else{
            url.getPort
            })
    private val buf=new Array[Byte](1)
    private val in=socket.getInputStream
    private val out=new DataOutputStream(socket.getOutputStream)
    private var isEnd=false
    var proxyResponseHeader=Map[String, String]()

    private def getChar() :Byte={
        val readSize=in.read(buf, 0, 1)
        if(readSize == -1){
            isEnd=true
            throw new IOException
        }
        buf(0)
    }

    private def getLine() :Array[Byte]={
        val buf=new ArrayBuffer[Byte]
        try {
            var endOfLine=false
            while(!endOfLine){
                getChar() match {
                    case '\n'=> endOfLine=true
                    case '\r'=> 0
                    case b => buf.append(b)
                }
            }
        }
        catch{
            case e :IOException=> 0
        }
        buf.toArray
    }

    private def readHeader(){
        var inHeader=true
        while(inHeader){
            val line=new String(getLine())
            if(line==""){
                inHeader=false
            }
            else{
                line match {
                    case HTTPResponse.HeaderPattern(key, value) =>
                        proxyResponseHeader+=(key.toLowerCase() -> value)
                }
            }
        }
    }

    private def getData(size :Int) :(Int, Array[Byte])={
        if(isEnd){
            return (0 -> new Array[Byte](0))
        }

        val buf=new Array[Byte](size)
        val readSize=in.read(buf, 0, buf.length)
        if(readSize == -1){
            isEnd=true
                return (0 -> new Array[Byte](0))
        }

        return (readSize -> buf)
    }

    private def publishBody(socket :Socket){
        val out=new DataOutputStream(socket.getOutputStream())
        out.writeBytes("HTTP/1.0 200 OK\r\n")
        out.writeBytes("Content-Type: "+
                proxyResponseHeader("content-type")+"\r\n")
        out.writeBytes("\r\n")
        if(proxyResponseHeader.contains("content-length")){
            val (size, buf)=getData(
                proxyResponseHeader("content-length") toInt)
            out.write(buf, 0, size)
        }
        else{
            while(!isEnd){
                val (size, buf)=getData(1024)
                out.write(buf, 0, size)
            }
        }
        out.flush()
        socket.close()
    }

    def get(client :Socket) :Boolean={
        // send request
        var header=new ProxyRequest("GET", url, "1.0")
        header.send(out)
        out.writeBytes("\r\n")
        out.flush()
        // read header
        val firstline=new String(getLine())
        firstline match {
            case HTTPResponse.SuccessPattern(version, code, message)=>
                println(code, message)
                readHeader()
                publishBody(client)
                false

            case HTTPResponse.RedirectPattern(version, code, message)=>
                println(code, message)
                readHeader()
                if(proxyResponseHeader("location")==
                        "http://"+url.getHost+url.getPath){
                    println("infinity loop")
                        false
                }
                else{
                    true
                }

            case HTTPResponse.Pattern(version, code, message)=>
                println(code, message)
                false

                case _ =>
                println("no match: "+firstline)
                false
        }
    }

}

class HTTPClient(client :Socket, index :Int, version :String) {

    def get(url :String){
        var redirect=0
        var target=new URL(url)
        val maxRedirection=3
        while(redirect<maxRedirection){
            println("["+index+":"+redirect+"] GET: "+target+" "+version)
            val c=new ProxyConnection(target)
            val isRedirected=c.get(client)

            if(isRedirected){
                // rdcirection
                redirect+=1
                    val location=c.proxyResponseHeader("location")
                    println("Location: "+location)
                    try{
                        target=if(target.getQuery==null){
                            new URL(location)
                        }
                        else{
                            new URL(location + "?" + target.getQuery())
                        }
                    }
                catch{
                case e :MalformedURLException=>
                        target=if(target.getQuery==null){
                            new URL(
                                    "http://"+target.getHost()+location)
                        }
                        else{
                            new URL(
                                    "http://"+target.getHost()+location+
                                    "?"+target.getQuery())
                        }
                }
                if(redirect>=maxRedirection){
                    println("abort redirection")
                }
            }
            else{
                // no redirection
                redirect=maxRedirection
            }
        }
    }

    def post(url :String){
        "not implemented"
    }

}