using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace _2chAPIKage
{
/// <summary>
/// 2ch DAT→2ch API変換串のサンプル
/// SessionIDの期限とかしくじった時のエラーコードとかが判らんから適当。
/// このソースの元になったソース
/// C# と .NET Framework で作る簡単プロキシサーバ
/// http://d.hatena.ne.jp/wwwcfe/20081228/1230470881
/// 2ch APIにポストする部分
/// WebRequest/WebResponseクラスでPOSTメソッドによりデータを送信するには?
/// http://www.atmarkit.co.jp/fdotnet/dotnettips/318webpost/webpost.html
/// 差分取得に対応させるための部分
/// http://rsdn.ru/forum/dotnet/1966333.all
/// 2chAPIの仕様書
/// http://codepad.org/9ZfVq5aZ
/// 認証メソッドはここからベタ移植
/// http://codepad.org/6M2KhlKR
/// </summary>
class HttpServer
{
HttpListener listener;
private const string AppKey = "自分で調べろ";
private const string HMKey = "自分で調べろ";
public bool IsListening
{
get
{
return (listener != null) && listener.IsListening;
}
}
public HttpServer()
{
listener = null;
}
public void Start()
{
if (listener != null)
return;
Debug.WriteLine("Enter HttpServer::Start");
listener = new HttpListener();
listener.Prefixes.Add(string.Format("http://{0}:{1}/", IPAddress.Loopback, 8081));
listener.Start();
listener.BeginGetContext(EndGetContext, listener);
Debug.WriteLine("Exit HttpServer::Start");
}
public void Stop()
{
if (listener == null)
return;
Debug.WriteLine("Enter HttpServer::Stop");
listener.Stop();
listener.Close();
listener = null;
Debug.WriteLine("Exit HttpServer::Stop");
}
private void EndGetContext(IAsyncResult ar)
{
Debug.WriteLine("Enter HttpServer::EndGetContext");
HttpListener listener = ar.AsyncState as HttpListener;
if (!listener.IsListening) // 呼び出した listener が Stop されているなら何もしない
{
Debug.WriteLine("Exit HttpServer::EndGetContext listener stopped");
return;
}
HttpListenerContext context = null;
try
{
listener.BeginGetContext(EndGetContext, listener);
context = listener.EndGetContext(ar);
HandleRequest(context);
}
catch (Exception ex)
{
Debug.WriteLine("Exception HttpServer::EndGetContext " + ex.Message);
if (context != null)
context.Response.Abort();
}
finally
{
if (context != null)
context.Response.Close();
Debug.WriteLine("Exit HttpServer::EndGetContext");
}
}
private HttpWebRequest CreateRequest(HttpListenerContext context, bool keepalive)
{
// HttpListenerRequest と同じ HttpWebRequest を作る。
HttpListenerRequest req = context.Request;
HttpWebRequest webRequest = WebRequest.Create(req.RawUrl) as HttpWebRequest;
webRequest.Method = req.HttpMethod;
webRequest.ProtocolVersion = HttpVersion.Version11;
// 接続してきたクライアントが切断しても、ほかのクライアントでこの WebRequest の接続を再利用できるはずなので常に true でもいいか?
webRequest.KeepAlive = keepalive;
// HttpWebRequest の制限がきついのでヘッダごとに対応
for (int i = 0; i < req.Headers.Count; i++)
{
string name = req.Headers.GetKey(i).ToLower();
string value = req.Headers.Get(i).ToLower();
switch (name)
{
case "host":
break; // WebRequest.Create で適切に設定されているはず あとで確認
case "connection":
case "proxy-connection":
webRequest.KeepAlive = keepalive; // TODO: keepalive の取得はここで行う。
break;
case "referer":
webRequest.Referer = value;
break;
case "user-agent":
webRequest.UserAgent = value;
break;
case "accept":
webRequest.Accept = value;
break;
case "content-length":
webRequest.ContentLength = req.ContentLength64;
break;
case "content-type":
webRequest.ContentType = value;
break;
case "if-modified-since":
webRequest.IfModifiedSince = DateTime.Parse(value);
break;
case "range":
{
string rangesStr = value.Split('=')[1];
foreach (string rangeStr in rangesStr.Split(','))
{
string[] ranges = rangeStr.Split(new char[] { '-' }, StringSplitOptions.None);
if (ranges[0] == "")
{
if (ranges[1] == "")
{
}
else
{
webRequest.AddRange(-int.Parse(ranges[1]));
}
}
else
{
if (ranges[1] == "")
{
webRequest.AddRange(int.Parse(ranges[0]));
}
else
{
webRequest.AddRange(int.Parse(ranges[0]), int.Parse(ranges[1]));
}
}
}
}
break;
default:
try
{
// その他。上以外にも個別に対応しなければならないものがあるが面倒なのでパス
webRequest.Headers.Set(name, value);
}
catch
{
Debug.WriteLine("Exception HttpServer::CreateRequest header=" + name);
}
break;
}
}
return webRequest;
}
public string Authenticate2ChApi()
{
WebClient wc = new WebClient();
string CT = "1234567890";
string message = AppKey + CT;
string url = "https://api.2ch.net/v1/auth/";
HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(HMKey));
string HB = BitConverter.ToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLower().Replace("-", "");
hmac.Clear();
NameValueCollection ps = new NameValueCollection();
ps.Add("ID", "");
ps.Add("PW", "");
ps.Add("KY", AppKey);
ps.Add("CT", CT);
ps.Add("HB", HB);
wc.Headers.Add(HttpRequestHeader.UserAgent, "Monazilla/1.3");
wc.Headers.Add("X-2ch-UA", "JaneStyle/3.80");
byte[] resData = wc.UploadValues(url, ps);
wc.Dispose();
return Encoding.UTF8.GetString(resData).Split(':')[1];
}
private HttpWebRequest Create2chRequest(HttpListenerContext context, string serverName, string boardName, string threadId,string sid, bool keepalive)
{
// 2ch API 専用の HttpWebRequest を作る。
HttpListenerRequest req = context.Request;
// POSTする内容の作成
string url = "https://api.2ch.net/v1/" + serverName + "/" + boardName + "/" + threadId;
string message = "/v1/" + serverName + "/" + boardName + "/" + threadId + sid + AppKey;
HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(HMKey));
string hobo = BitConverter.ToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLower().Replace("-", "");
hmac.Clear();
HttpWebRequest webRequest = WebRequest.Create(url) as HttpWebRequest;
Hashtable ht = new Hashtable();
ht["sid"] = sid;
ht["hobo"] = hobo;
ht["appkey"] = AppKey;
string param = ht.Keys.Cast<string>().Aggregate("", (current, k) => current + String.Format("{0}={1}&", k, ht[k]));
byte[] data = Encoding.ASCII.GetBytes(param);
//ヘッダを設定
webRequest.Method = "POST";
webRequest.UserAgent = "Mozilla/3.0 (compatible; JaneStyle/3.80..)";
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.ProtocolVersion = HttpVersion.Version11;
webRequest.ContentLength = data.Length;
// 接続してきたクライアントが切断しても、ほかのクライアントでこの WebRequest の接続を再利用できるはずなので常に true でもいいか?
webRequest.KeepAlive = keepalive;
// 2ch専用ブラウザ側から受取ったヘッダを追加
for (int i = 0; i < req.Headers.Count; i++)
{
string name = req.Headers.GetKey(i).ToLower();
string value = req.Headers.Get(i).ToLower();
switch (name)
{
case "connection":
case "proxy-connection":
webRequest.KeepAlive = keepalive; // TODO: keepalive の取得はここで行う。
break;
case "accept":
webRequest.Accept = value;
break;
case "if-modified-since":
webRequest.IfModifiedSince = DateTime.Parse(value);
break;
case "range":
{
string rangesStr = value.Split('=')[1];
foreach (string rangeStr in rangesStr.Split(','))
{
string[] ranges = rangeStr.Split(new char[] { '-' }, StringSplitOptions.None);
if (ranges[0] == "")
{
if (ranges[1] == "")
{
}
else
{
webRequest.AddRange(-int.Parse(ranges[1]));
}
}
else
{
if (ranges[1] == "")
{
webRequest.AddRange(int.Parse(ranges[0]));
}
else
{
webRequest.AddRange(int.Parse(ranges[0]), int.Parse(ranges[1]));
}
}
}
}
break;
}
}
//POSTする内容を書き込み
Stream webRequestStream = webRequest.GetRequestStream();
webRequestStream.Write(data, 0, data.Length);
webRequestStream.Close();
return webRequest;
}
private void HandleRequest(HttpListenerContext context)
{
// どういう処理をさせるのかを決める。
HttpListenerRequest req = context.Request;
HttpListenerResponse res = context.Response;
// どこから接続されたかと、加工されていないアドレス
Debug.WriteLine("Info HttpServer::HandleRequest " + string.Format("UserHost={0}: Request={1}", req.UserHostAddress, req.RawUrl));
bool keepalive = req.KeepAlive; // 常に false らしい。バグらしい。
if (!string.IsNullOrEmpty(req.Headers["Connection"]) && req.Headers["Connection"].IndexOf("keep-alive", StringComparison.InvariantCultureIgnoreCase) >= 0 ||
!string.IsNullOrEmpty(req.Headers["Proxy-Connection"]) && req.Headers["Proxy-Connection"].IndexOf("keep-alive", StringComparison.InvariantCultureIgnoreCase) >= 0)
keepalive = true; // バグ対策?
if (req.RawUrl.StartsWith("/") || req.RawUrl.StartsWith("http://local.ptron/"))
ProcessLocalRequest(context, keepalive);
else // プロクシサーバとしての振る舞い
ProcessProxyRequest(context, keepalive);
}
private void ProcessProxyRequest(HttpListenerContext context, bool keepalive)
{
// プロキシサーバとしての動作。
HttpListenerRequest req = context.Request;
HttpListenerResponse res = context.Response;
// もし2chのDATのURLだったらRequestをAPI用に加工する。
Regex regex = new Regex(@"http://(\w+)\.2ch\.net:80/(\w+)/dat/(\d+)\.dat");
Match match = regex.Match(req.RawUrl);
HttpWebRequest webRequest = null;
if (match.Success)
{
string sid = "";
try
{
Debug.WriteLine("Info HttpServer::ProcessProxyRequest Authenticate2ChApi Start");
sid = Authenticate2ChApi();
Debug.WriteLine("Info HttpServer::ProcessProxyRequest 2ch API sid :"+sid);
}
catch (WebException exp)
{
SendResponse(context, 502, "Bad Gateway",keepalive, null);
return;
}
webRequest = Create2chRequest(context, match.Groups[1].Value, match.Groups[2].Value, match.Groups[3].Value, sid, keepalive);
}
else
{
webRequest = CreateRequest(context, keepalive);
}
// ボディあったら送受信
if (req.HasEntityBody) // リクエストのボディがある (POST とか)
Relay(req.InputStream, webRequest.GetRequestStream());
if (webRequest == null)
{SendResponse(context, 502, "Bad Gateway", keepalive, null);
return;}
// レスポンス取得
HttpWebResponse webResponse = null;
try
{
webResponse = webRequest.GetResponse() as HttpWebResponse;
}
catch (WebException e)
{
webResponse = e.Response as HttpWebResponse; // レスポンスがあればとる。304 とかの場合。なければ null になる。
Debug.WriteLine("Exception HttpServer::ProcessProxyRequest " + e.Message);
}
// だめだった時の処理。てきとう
if (webResponse == null)
{
SendResponse(context, 502, "Bad Gateway", keepalive, null);
return;
}
// ブラウザへ返すレスポンスの設定。あるていど。
res.ProtocolVersion = HttpVersion.Version11; // 常に HTTP/1.1 としておく
res.StatusCode = (int)webResponse.StatusCode;
res.StatusDescription = webResponse.StatusDescription;
res.KeepAlive = keepalive;
for (int i = 0; i < webResponse.Headers.Count; i++)
{
string name = webResponse.Headers.GetKey(i).ToLower();
string value = webResponse.Headers.Get(i).ToLower();
switch (name)
{
case "content-length":
res.ContentLength64 = webResponse.ContentLength;
break;
case "keep-alive": // どうやって設定しようか...
Debug.WriteLine("Info HttpServer::ProcessProxyRequest keep-alive: " + value);
break;
case "transfer-encoding":
res.SendChunked = value.IndexOf("chunked") >= 0 ? true : false;
break;
default:
try
{
res.Headers.Set(name, value);
}
catch
{
Debug.WriteLine("Exception HttpServer::ProcessProxyRequest header=" + name);
}
break;
}
}
Relay(webResponse.GetResponseStream(), res.OutputStream);
webResponse.Close();
}
private void ProcessLocalRequest(HttpListenerContext context, bool keepalive)
{
HttpListenerRequest req = context.Request;
// 通常の HTTP サーバとしての振る舞い。または local.ptron へのアクセス。
if (req.RawUrl.Equals("/") || req.RawUrl.Equals("http://local.ptron/"))
SendResponse(context, 200, "OK", keepalive, Encoding.Default.GetBytes("Hello World"));
else // favicon とか取りに来るので。
SendResponse(context, 404, "Not Found", keepalive, Encoding.Default.GetBytes("404 not found"));
}
private void Relay(Stream input, Stream output)
{
// Stream から読めなくなるまで送受信。
byte[] buffer = new byte[4096];
while (true)
{
int bytesRead = input.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
break;
output.Write(buffer, 0, bytesRead);
}
input.Close();
output.Close();
}
private void SendResponse(HttpListenerContext context, int code, string description, bool keepalive, byte[] body)
{
context.Response.StatusCode = code;
context.Response.StatusDescription = description;
context.Response.ProtocolVersion = HttpVersion.Version11;
context.Response.KeepAlive = keepalive;
if (body != null)
{
context.Response.ContentType = "text/plain";
context.Response.ContentLength64 = body.Length;
context.Response.OutputStream.Write(body, 0, body.Length);
context.Response.OutputStream.Close();
}
else
{
context.Response.ContentLength64 = 0;
}
}
private void SendFile(HttpListenerContext context, int code, string description, bool keepalive, byte[] body, string contentType)
{
context.Response.StatusCode = code;
context.Response.StatusDescription = description;
context.Response.ProtocolVersion = HttpVersion.Version11;
context.Response.KeepAlive = keepalive;
if (body != null)
{
context.Response.ContentType = contentType;
context.Response.ContentLength64 = body.Length;
context.Response.OutputStream.Write(body, 0, body.Length);
context.Response.OutputStream.Close();
}
else
{
context.Response.ContentLength64 = 0;
}
}
}
}