Pull to refresh

Azure. Отладка приложений на 80-м порту

Reading time10 min
Views580
Разрабатывая приложение на платформе Windows Azure нам приходилось мириться с особенностью запуска debug-сборок на разных портах. Но когда мы начали активно отлаживать кросс-доменный AJAX эта проблема встала особенно остро. Потому как скрипты требовали использования абсолютного URL в тексте js-скрипта. Нам пришлось писать номер порта в URL: http://127.0.0.1:81/bla-bla-bla. Когда при перезапуске приложения порт изменялся — приходилось перезапускать devfabric для того, чтобы сборка запускалась на 81-ом порту. Перезапуск отнимал драгоценное время, раздражение возрастало.
В один прекрасный момент терпение лопнуло и мы решили создать инструмент для отладки приложения на одном порту. Он представляет из себя ASPX-приложение, которое получает запросы и перенаправляет их на запущенную инстанцию Azure. Это позволяет нам не заботиться о том, на каком же порту запущен сейчас Azure.

Предварительная настройка


В процессе разработки родилась идея написать что-то вроде Reverse Proxy для того чтобы инстанция Azure была доступна по адресу azureproxy.com. Для этого мы используем IIS 7.5, который все равно крутится на машине.
В диспетчере служб IIS добавляем пул приложений AzureProxy
Добавление пула приложений
Версию среды .Net Framework указываем четвертую.
И добавляем веб-сайт
Добавление сайта
Здесь нужно указать имя сайта «AzureProxy», пул приложений «AzureProxy», физический путь — любой (здесь D:\AzureProxy\). Также нужно указать привязку: IP-адрес — 127.0.0.1, Порт — 80, Имя узла — azureproxy.com.
Также нам понадобится модуль Url Rewrite для IIS. Если он у вас ещё не установлен — необходимо посетить www.iis.net/download/URLRewrite и установить его.
Следующим шагом будет добавление azureproxy.com в файл hosts.
В редакторе открываем файл C:\Windows\System32\drivers\etc\hosts и добавляем в него строку
127.0.0.1 azureproxy.com
Предварительный этап закончен.

Программируем


Настало время открыть Visual Studio 2010.
Создаем новый проект
Создание нового проекта
Тип проекта — ASP .NET Empty Web Application, остальное по желанию.
Правим файл Web.Config
<?xml version="1.0"?>
<configuration>
 <system.web>
  <compilation debug="true" targetFramework="4.0" />
  <pages enableViewStateMac="false" />
 </system.web>
 <system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
   <add name="AzureProxyModule" type="AzureProxy.AzureProxyModule, AzureProxy" />
  </modules>
  <rewrite>
   <rules>
    <rule name="All" stopProcessing="true">
     <match url="^(.*)$" />
     <action type="Rewrite" url="/Default.aspx?url={HtmlEncode:{R:0}}" />
    </rule>
   </rules>
  </rewrite>
 </system.webServer>
</configuration>

* This source code was highlighted with Source Code Highlighter.


Добавляем новую веб-форму
Новая веб-форма
с именем Default.aspx
И оставляем в ней только
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="AzureProxy.Default" %>


Далее добавляем новый класс
Новый класс
c именем AzureProxyModule.cs следующего содержания

using System;
using System.Net;
using System.IO;
using System.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;

namespace AzureProxy
{
  public class AzureProxyModule : IHttpModule
  {

    const string Domain = "127.0.0.1";
    const int MaxPort = 110;

    public static string Port = "";
    public Queue<int> ports = new Queue<int>();
    public List<Thread> threads = new List<Thread>();
    public bool ThreadsLive = false;

    public AzureProxyModule()
    {
    }

    public String ModuleName
    {
      get { return "AzureProxyModule"; }
    }

    public void Init(HttpApplication httpApp)
    {

      httpApp.BeginRequest +=
        new EventHandler(this.OnBeginRequest);

      httpApp.EndRequest +=
        new EventHandler(this.OnEndRequest);
    }

    public void OnBeginRequest(object o, EventArgs ea)
    {
    }

    public void OnEndRequest(object o, EventArgs ea)
    {
      HttpApplication httpApp = (HttpApplication)o;
      if (Port.Length == 0)
      {
        SearchPort();
      }

      DoRequest();

    }

    public void DoRequest()
    {

      byte[] buffer = new byte[4096];
      HttpContext ctx;
      ctx = HttpContext.Current;
      if (ctx.Request.QueryString["url"] == null) return;

      HttpWebRequest myHttp = (HttpWebRequest)HttpWebRequest.Create("http://" + Domain + ":" + Port + ctx.Request.RawUrl.ToString());
      myHttp.AllowAutoRedirect = false;
      myHttp.KeepAlive = true;
      myHttp.CookieContainer = new CookieContainer();
      myHttp.UserAgent = ctx.Request.UserAgent;
      foreach (string CookieName in ctx.Request.Cookies.AllKeys)
      {
        if (ctx.Request.Cookies[CookieName].Domain != null)
        {
          myHttp.CookieContainer.Add(new Cookie(ctx.Request.Cookies[CookieName].Name, ctx.Request.Cookies[CookieName].Value, ctx.Request.Cookies[CookieName].Path, ctx.Request.Cookies[CookieName].Domain));
        }
        else
        {
          myHttp.CookieContainer.Add(new Cookie(ctx.Request.Cookies[CookieName].Name, ctx.Request.Cookies[CookieName].Value, ctx.Request.Cookies[CookieName].Path, Domain));
        }
      }
      myHttp.ContentType = ctx.Request.ContentType;

      if (ctx.Request.HttpMethod == "POST")
      {
        myHttp.Method = "POST";
        myHttp.AllowWriteStreamBuffering = true;
        myHttp.ContentLength = ctx.Request.InputStream.Length;
        Stream requestStream = myHttp.GetRequestStream();
        int length = 0;
        while ((length = ctx.Request.InputStream.Read(buffer, 0, buffer.Length)) > 0)
        {
          requestStream.Write(buffer, 0, length);
        }
      }

      try
      {
        WebResponse response = myHttp.GetResponse();

        if (response.Headers["Location"] != null)
        {
          ctx.Response.Redirect(response.Headers["Location"]);
        }

        using (Stream stream = response.GetResponseStream())
        {
          using (MemoryStream memoryStream = new MemoryStream())
          {
            int count = 0;
            do
            {
              count = stream.Read(buffer, 0, buffer.Length);
              memoryStream.Write(buffer, 0, count);

            } while (count != 0);

            ctx.Response.ContentType = response.ContentType;
            foreach (string HeaderKey in response.Headers.Keys)
            {
              ctx.Response.AddHeader(HeaderKey, response.Headers[HeaderKey]);
            }

            ctx.Response.BinaryWrite(memoryStream.ToArray());

          }
        }
      }
      catch (WebException code)
      {
        switch (code.Message)
        {
          case "Unable to connect to the remote server":
            SearchPort();
            DoRequest();
            break;
          case "The remote server returned an error: (404) Not Found.":
            ctx.Response.Status = "404 File Not Found";
            break;
        }
        return;        
      }
    }

    public void SearchPort()
    {
      Port = "";
      for (int port = 81; port < MaxPort; port++)
      {
        ports.Enqueue(port);
        threads.Add(new Thread(new ThreadStart(PortTest)));
      }
      threads.ForEach(new Action<Thread>(ThreadStart));

      while (TreadsIsLive() && (Port.Length == 0))
      {
        //ждем ответа от трэдов
      }

      //завершаем все трэды
      threads.ForEach(new Action<Thread>(ThreadAbort));
    }

    public void Dispose() { }

    public bool TreadsIsLive()
    {
      ThreadsLive = false;
      threads.ForEach(new Action<Thread>(ThreadTest));
      return true;
    }

    public void ThreadTest(Thread t)
    {
      ThreadsLive = ThreadsLive || t.IsAlive;
    }

    protected void ThreadStart(Thread t)
    {
      try
      {
        ThreadsLive = true;
        if (!t.IsAlive) t.Start();
      }
      catch { }
    }

    protected void ThreadAbort(Thread t)
    {
      t.Abort();
    }

    protected void PortTest()
    {
      int port;
      try
      {
        port = ports.Dequeue();

        HttpWebRequest myHttpWebRequest =
  (HttpWebRequest)HttpWebRequest.Create("http://" + Domain + ":" + port.ToString());
        try
        {
          HttpWebResponse myHttpWebResponse =
          (HttpWebResponse)myHttpWebRequest.GetResponse();
          Port = port.ToString();
          return;
        }
        catch { }
      }
      catch { }
    }
  }
}

* This source code was highlighted with Source Code Highlighter.


Собираем приложение, и публикуем его в IIS
Публикация

Теперь инстанция Azure доступна по адресу azureproxy.com.

Заключение


Использованная техника позволяет проксировать запросы к запущенным инстанциям Azure с одного адреса. Поиск доступного порта осуществляется в дочерних трэдах параллельно, что позволяет быстро находить порт. Проксируются как GET так и POST запросы, входящий поток полностью передается инстанции Azure что позволяет загружать файлы.

P.S. В статье приведено сокращенное пошаговое руководство по созданию AzureProxy. Если потребуется — можем более подробно осветить проблемные места, которые возникали в процессе написания приложения.
Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments0

Articles